36 Commits

Author SHA1 Message Date
Mikhail Tyukin
8b8d58854f Fix a few bugs in server selector, do not hardcode paths 2025-12-10 15:14:49 -05:00
Mikhail Tyukin
5f27e2ea44 remove duplicate GetClearedMissions 2025-12-10 14:50:37 -05:00
qmengz
55e5cdcdb8 feat: Implement event mission and shop - 实现活动任务和商店 (#70)
- Removed obsolete GetClearList handler and replaced it with GetMissionClear and GetMissionClearList handlers for improved clarity and functionality.
- Introduced ObtainMissionReward handler to manage mission reward claims.
- Added BuyProduct and MultipleBuyProduct handlers to facilitate shop product purchases.
- Implemented EventShopHelper class to encapsulate shop-related logic, including product buying and currency deduction.
- Updated EventStoryHelper to manage event ticket logic and user event data.
- Enhanced User and EventData models to support new event and shop functionalities.
- Improved logging and error handling across various handlers.
2025-12-10 12:53:08 -05:00
Mikhail Tyukin
a45dd2305e fix burst skills and missing mission thingy 2025-12-08 18:16:26 -05:00
Mikhail Tyukin
c96ad5fa94 correctly count field object count 2025-12-08 15:17:18 -05:00
qmengz
91cbedf46c feat: Implement AZX mini-game functionality and related event handling - 实现 AZX 小游戏功能及相关事件处理 (#69)
- Added new classes for handling AZX mini-game events including entering, finishing, and retrieving data.
- Introduced helper methods for managing rewards, rankings, and achievements within the AZX mini-game.
- Updated existing event handling to include user-specific data for events and mini-games.
- Enhanced user model to store AZX mini-game data and private banner IDs.
- Modified event response structures to accommodate new data fields related to AZX mini-game.
- Implemented logging for better traceability of AZX mini-game actions and errors.
2025-12-06 20:55:31 -05:00
Mikhail Tyukin
913c1a2262 begin implementing story mode 2025-12-06 12:14:01 -05:00
Mikhail Tyukin
fb062eefae Fix unlocking of hard mode/story mode/surface mode 2025-12-06 11:42:05 -05:00
Mikhail Tyukin
bc7416c44a Update dotnet-desktop.yml 2025-12-05 15:33:04 -05:00
Mikhail Tyukin
4325a91cea fix linux build 2025-12-05 15:29:44 -05:00
qmengz
24f6de8704 feat: Implement SimRoom Overclock-实现模拟室超频 (#68)
- Added GetAcquireBuffFunction to handle buff acquisition requests in the simulation room.
- Added InfinitePopupCheck to manage infinite popup checks for users.
- Added ProceedSkipFunction to process skip requests in the simulation room.
- Updated SelectDifficulty to handle overclock options and season data.
- Enhanced SimRoomHelper to support overclock mechanics and event group logic.
- Modified GetSimRoomData to include buffs and legacy buffs in the response.
- Updated Quit functionality to reset overclock state upon quitting the simulation room.
- Added logic to handle overclock rewards and high score updates.
- Refactored user model to retain current season data and legacy buffs during resets.
- Introduced new OverclockData and OverclockHighScoreData models to manage overclock states.
2025-12-05 15:26:14 -05:00
Mikhail Tyukin
6d44c6eac9 Update dotnet-desktop.yml 2025-12-04 21:26:18 -05:00
Mikhail Tyukin
23afbabfae Compile server for linux 2025-12-04 19:50:22 -05:00
Mikhail Tyukin
f4e2d82978 fix side story unread 2025-12-04 17:20:31 -05:00
Mikhail Tyukin
2b92e3191b fix random boxes 2025-12-04 11:06:33 -05:00
Mikhail Tyukin
663bf58549 update to .net 10 2025-12-04 11:03:56 -05:00
Mikhail Tyukin
aac1c00715 Update ServerSwitcher.cs 2025-12-04 09:38:35 -05:00
Mikhail Tyukin
338a769ade fix serverselector 2025-12-04 09:37:12 -05:00
Mikhail Tyukin
d877488d1f add sidestory setviewed 2025-12-03 22:33:37 -05:00
Mikhail Tyukin
d378b68611 140.8.8 update pt 2 2025-12-03 22:26:47 -05:00
Mikhail Tyukin
3ffbfca0a3 update to 140.8.8 pt 1 2025-12-03 21:26:40 -05:00
Mikhail Tyukin
2b19500b67 remove unused code in serverselector, update packages 2025-11-26 22:38:00 -05:00
qmengz
5dc9945100 新增功能:实现包含简易模式特性的模拟房间功能 (#66)
feat: Implement SimRoom functionality with Simple Mode features

- Added SimRoomHelper class to manage SimRoom events and logic.
- Implemented SimpleModeSelectBuff handler for buff selection in Simple Mode.
- Implemented SimpleModeSetSkipOption to enable/disable skip options.
- Implemented SimpleModeSkipAll to handle skipping all Simple Mode stages and reward retrieval.
- Implemented SimpleModeSkipBuffSelection for skipping buff selection.
- Implemented SimpleModeStart to initiate Simple Mode with event handling.
- Updated SimRoom data models to include buffs, legacy buffs, and event tracking.
- Updated GameData add SimRoom data tables
-Added JsonStaticDataReplenish add SimRoom data models
- Enhanced User model to manage weekly reset logic and retain legacy buffs.
- Added DateTimeHelper utility for managing time zone specific date calculations.
- Updated game configuration for static data and resource URLs.
2025-11-26 10:54:04 -05:00
qmengz
93470b21a4 修复 SetTeam 逻辑中的内容 Id 处理并优化响应团队数据的添加方式 (#65)
Fixed ContentsId handling in the SetTeam logic and optimized the way response team data is added.
2025-11-11 16:37:10 -05:00
qmengz
16bd4077dd Add information about the third anniversary event (#62)
* Add information about the third anniversary event
2025-11-07 22:44:00 -05:00
Vi-brance
f09d959220 Fix Archive Story Scenario completion (#63) 2025-11-07 22:39:25 -05:00
Vi-brance
795f18445c implement /outpost/RecycleRoom/PersonalResearchLevelUp for RecycleRoom (#64) 2025-11-05 07:42:34 -05:00
Vi-brance
b8dc78e1c9 Fixed the issue where the "completestage" cmd could not unlock bonus stages after Chapter 39 with new string format (#61) 2025-11-01 09:24:41 -04:00
TTBB
dd2760b0c6 fix favorite item great success and fix obtain from harmony cube lost sector (#60) 2025-10-30 08:00:05 -04:00
Mikhail Tyukin
0aed2aff6c fix build 2025-10-29 22:03:06 -04:00
Mikhail Tyukin
2ee95caf32 Reapply "remove li pass popup"
This reverts commit 076f987681.
2025-10-29 22:01:33 -04:00
Mikhail Tyukin
9a8db1c143 WIP: update to 139 2025-10-29 19:45:14 -04:00
qmengz
eea01945f9 Add event pass functionality and logging improvements (#59)
- Implement FastClearEventStage handler for fast clearing event stages.
- Enhance GetStoryDungeon handler to include team data and cleared stages.
- Modify GetInventoryData to streamline item processing.
- Introduce BuyEventPassRank and BuyPassRank handlers for purchasing event pass ranks.
- Add CompleteEventPassMission and CompletePassMission handlers for mission completion.
- Create ObtainEventPassReward and ObtainOneEventPassReward handlers for reward retrieval.
- Implement ObtainPassReward and ObtainOnePassReward handlers for general pass rewards.
- Add PassHelper class to manage pass-related logic, including obtaining rewards and completing missions.
- Update User and EventData models to support new pass functionality.
- Integrate log4net for improved logging capabilities throughout the application.
- Update game configuration for static data and resource URLs.
- Create log4net configuration file for logging setup.
2025-10-20 10:34:09 -04:00
fxz2018
206fa429ee feat: implement equipment awakening system with interception updates (#57)
- Implement comprehensive equipment awakening system with multiple new endpoints:
  * Awakening.cs: Handle equipment awakening process
  * ChangeOption.cs: Change equipment awakening options
  * GetAwakeningDetail.cs: Get detailed awakening information
  * LockOption.cs: Lock awakening options (with Disposable option)
  * ResetOption.cs: Reset awakening options
  * UpgradeOption.cs: Upgrade awakening options

- Enhance interception system:
  * Simplify GetInterceptData to use fixed normal group ID (1)
  * Update InterceptionHelper to support type 1 in addition to type 0
  * Modify GetInterceptData to use simplified special ID calculation

- Implement new inventory system features:
  * Replace ClearAllEquipment with AllClearEquipment
  * Enhance GetInventoryData to properly handle HarmonyCubes and Awakenings
  * Update WearEquipmentList for improved equipment management

- Update data models and game data:
  * Modify GetConditionReward to handle valueMax == 0 cases
  * Update EquipmentAwakeningData model with proper default values
  * Update ResetableData with DailyCounselCount as dictionary instead of struct field

- Additional improvements:
  * Create GameAssemblyProcessor utility
  * Enhance level infinite controller
  * Update server selector UI
  * Organize protocol message documentation
2025-10-07 21:30:27 -04:00
Mikhail Tyukin
cbbefeb51a fix find/replace mistakes 2025-09-29 21:18:46 -04:00
Mikhail Tyukin
076f987681 Revert "remove li pass popup"
This reverts commit 996b585500.
2025-09-29 18:46:25 -04:00
150 changed files with 923091 additions and 785264 deletions

View File

@@ -2,3 +2,30 @@
# CA5397: Do not use deprecated SslProtocols values
dotnet_diagnostic.CA5397.severity = none
# CS8602: 解引用可能出现空引用。
dotnet_diagnostic.CS8602.severity = none
# CS8619: 值中的引用类型的为 Null 性与目标类型不匹配。
dotnet_diagnostic.CS8619.severity = none
# CS8604: 引用类型参数可能为 null。
dotnet_diagnostic.CS8604.severity = none
# CS8981: 该类型名称仅包含小写 ascii 字符。此类名称可能会成为该语言的保留值。
dotnet_diagnostic.CS8981.severity = none
# IDE0120: 简化 LINQ 表达式
dotnet_diagnostic.IDE0120.severity = none
# CS8603: 可能返回 null 引用。
dotnet_diagnostic.CS8603.severity = none
# SYSLIB0039: 类型或成员已过时
dotnet_diagnostic.SYSLIB0039.severity = none
# CS0219: 变量已被赋值,但从未使用过它的值
dotnet_diagnostic.CS0219.severity = none
# CS8618: 类型不包含 null 值的属性
dotnet_diagnostic.CS8618.severity = none

View File

@@ -18,15 +18,15 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
# Install the .NET Core workload
- name: Install .NET 9
uses: actions/setup-dotnet@v4
- name: Install .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
# Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
- name: Setup MSBuild
@@ -42,10 +42,44 @@ jobs:
run: dotnet publish EpinelPS
- name: Copy to output
run: echo ${{ github.workspace }} && md ${{ github.workspace }}/out/ && xcopy /s /e "${{ github.workspace }}\ServerSelector.Desktop\bin\Release\net9.0\win-x64\publish\" "${{ github.workspace }}\out\" && xcopy /s /e "${{ github.workspace }}\EpinelPS\bin\Release\net9.0\win-x64\publish\" "${{ github.workspace }}\out\" && copy "${{ github.workspace }}\ServerSelector.Desktop\sodium.dll" "${{ github.workspace }}\out\sodium.dll"
run: echo ${{ github.workspace }} && md ${{ github.workspace }}/out/ && xcopy /s /e "${{ github.workspace }}\ServerSelector.Desktop\bin\Release\net10.0\win-x64\publish\" "${{ github.workspace }}\out\" && xcopy /s /e "${{ github.workspace }}\EpinelPS\bin\Release\net10.0\win-x64\publish\" "${{ github.workspace }}\out\" && copy "${{ github.workspace }}\ServerSelector.Desktop\sodium.dll" "${{ github.workspace }}\out\sodium.dll"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: Server and Server selector
path: ${{ github.workspace }}/out/
serverOnly:
strategy:
matrix:
configuration: [Release]
runs-on: ubuntu-latest # For a list of available runner types, refer to
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
# Install the .NET Core workload
- name: Install .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Restore packages
run: dotnet restore
- name: Publish Server
run: dotnet publish EpinelPS
- name: Copy to output
run: echo ${{ github.workspace }} && cp -R "${{ github.workspace }}/EpinelPS/bin/Release/net10.0/linux-x64/publish/" "${{ github.workspace }}/out/"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: EpinelPS_linux_x64
path: ${{ github.workspace }}/out/

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<AvaloniaVersion>11.0.2</AvaloniaVersion>
<AvaloniaVersion>11.3.9</AvaloniaVersion>
</PropertyGroup>
</Project>

View File

@@ -1,6 +1,7 @@
using System.Data;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using EpinelPS.Database;
using EpinelPS.Utils;
using ICSharpCode.SharpZipLib.Zip;
@@ -22,9 +23,7 @@ namespace EpinelPS.Data
}
}
public byte[] Sha256Hash;
public byte[] MpkHash = [];
public int Size;
public int MpkSize;
private ZipFile MainZip;
@@ -32,9 +31,6 @@ namespace EpinelPS.Data
private int totalFiles = 1;
private int currentFile;
// TODO: all of the data types need to be changed to match the game
private bool UseMemoryPack = true;
public readonly Dictionary<string, FieldMapRecord> MapData = [];
[LoadRecord("MainQuestTable.json", "Id")]
@@ -52,6 +48,9 @@ namespace EpinelPS.Data
[LoadRecord("CampaignChapterTable.json", "Chapter")]
public readonly Dictionary<int, CampaignChapterRecord> ChapterCampaignData = [];
[LoadRecord("ContentsOpenTable.json", "Id")]
public readonly Dictionary<ContentsOpen, ContentsOpenRecord> ContentsOpenTable = [];
[LoadRecord("CharacterCostumeTable.json", "Id")]
public readonly Dictionary<int, CharacterCostumeRecord> CharacterCostumeTable = [];
@@ -254,6 +253,105 @@ namespace EpinelPS.Data
[LoadRecord("EventMVGMissionTable.json", "Id")]
public readonly Dictionary<int, EventMVGMissionRecord_Raw> EventMvgMissionTable = [];
[LoadRecord("EquipmentOptionTable.json", "Id")]
public readonly Dictionary<int, EquipmentOptionRecord> EquipmentOptionTable = [];
[LoadRecord("EquipmentOptionCostTable.json", "Id")]
public readonly Dictionary<int, EquipmentOptionCostRecord> EquipmentOptionCostTable = [];
[LoadRecord("ItemEquipCorpSettingTable.json", "Id")]
public readonly Dictionary<int, ItemEquipCorpSettingRecord> ItemEquipCorpSettingTable = [];
[LoadRecord("LobbyPrivateBannerTable.json", "Id")]
public readonly Dictionary<int, LobbyPrivateBannerRecord> LobbyPrivateBannerTable = [];
[LoadRecord("LoginEventTable.json", "Id")]
public readonly Dictionary<int, LoginEventRecord> LoginEventTable = [];
// Contents Shop Data Tables
[LoadRecord("ContentsShopTable.json", "Id")]
public readonly Dictionary<int, ContentsShopRecord> ContentsShopTable = [];
[LoadRecord("ContentsShopProductTable.json", "Id")]
public readonly Dictionary<int, ContentsShopProductRecord> ContentsShopProductTable = [];
[LoadRecord("ShopDiscountProbTable.json", "Id")]
public readonly Dictionary<int, ShopDiscountProbRecord> ShopDiscountProbTable = [];
// Event Dungeon data Table
[LoadRecord("EventDungeonTable.json", "Id")]
public readonly Dictionary<int, EventDungeonRecord> EventDungeonTable = [];
[LoadRecord("EventDungeonStageTable.json", "Id")]
public readonly Dictionary<int, EventDungeonStageRecord> EventDungeonStageTable = [];
[LoadRecord("EventDungeonSpotBattleTable.json", "Id")]
public readonly Dictionary<int, EventDungeonSpotBattleRecord> EventDungeonSpotBattleTable = [];
[LoadRecord("EventDungeonDifficultTable.json", "Id")]
public readonly Dictionary<int, EventDungeonDifficultRecord> EventDungeonDifficultTable = [];
[LoadRecord("EventStoryTable.json", "Id")]
public readonly Dictionary<int, EventStoryRecord> EventStoryTable = [];
[LoadRecord("AutoChargeTable.json", "Id")]
public readonly Dictionary<int, AutoChargeRecord> AutoChargeTable = [];
// Pass Data Tables
[LoadRecord("PassManagerTable.json", "Id")]
public readonly Dictionary<int, PassManagerRecord> PassManagerTable = [];
[LoadRecord("EventPassManagerTable.json", "Id")]
public readonly Dictionary<int, EventPassManagerRecord> EventPassManagerTable = [];
[LoadRecord("SeasonPassTable.json", "Id")]
public readonly Dictionary<int, SeasonPassRecord> SeasonPassTable = [];
[LoadRecord("PassMissionTable.json", "Id")]
public readonly Dictionary<int, PassMissionRecord> PassMissionTable = [];
// Event Mission Data Tables
[LoadRecord("EventMissionListTable.json", "Id")]
public readonly Dictionary<int, EventMissionListRecord> EventMissionListTable = [];
[LoadRecord("EventMissionCategoryTable.json", "Id")]
public readonly Dictionary<int, EventMissionCategoryRecord> EventMissionCategoryTable = [];
// Daily Mission Event Data Tables
[LoadRecord("DailyMissionEventSettingTable.json", "Id")]
public readonly Dictionary<int, DailyMissionEventSettingRecord_Raw> DailyMissionEventSettingTable = [];
[LoadRecord("DailyEventTable.json", "Id")]
public readonly Dictionary<int, DailyEventRecord> DailyEventTable = [];
// SimulationRoom Data Tables
[LoadRecord("SimulationRoomChapterTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomChapterRecord> SimulationRoomChapterTable = [];
[LoadRecord("SimulationRoomStageLocationTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomStageLocationRecord> SimulationRoomStageLocationTable = [];
[LoadRecord("SimulationRoomSelectionEventTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomSelectionEventRecord> SimulationRoomSelectionEventTable = [];
[LoadRecord("SimulationRoomSelectionGroupTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomSelectionGroupRecord> SimulationRoomSelectionGroupTable = [];
[LoadRecord("SimulationRoomBattleEventTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomBattleEventRecord> SimulationRoomBattleEventTable = [];
[LoadRecord("SimulationRoomLevelScalingTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomLevelScalingRecord> SimulationRoomLevelScalingTable = [];
[LoadRecord("SimulationRoomBuffPreviewTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomBuffPreviewRecord> SimulationRoomBuffPreviewTable = [];
[LoadRecord("SimulationRoomBuffTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomBuffRecord> SimulationRoomBuffTable = [];
// SimulationRoom Overclock Data Tables
[LoadRecord("SimulationRoomOcLevelTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomOverclockLevelRecord> SimulationRoomOcLevelTable = [];
[LoadRecord("SimulationRoomOcOptionGroupTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomOverclockOptionGroupRecord> SimulationRoomOcOptionGroupTable = [];
[LoadRecord("SimulationRoomOcOptionTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomOverclockOptionRecord> SimulationRoomOcOptionTable = [];
[LoadRecord("SimulationRoomOcSeasonTable.json", "Id")]
public readonly Dictionary<int, SimulationRoomOverclockSeasonRecord> SimulationRoomOcSeasonTable = [];
// MiniGame AZX Tables
[LoadRecord("EventAZXAppleGameMissionTable.json", "Id")]
public readonly Dictionary<int, EventAZXAppleGameMissionRecord_Raw> EventAZXAppleGameMissionTable = [];
[LoadRecord("EventAZXAppleGameBoardTable.json", "Id")]
public readonly Dictionary<int, EventAZXAppleGameBoardRecord_Raw> EventAZXAppleGameBoardTable = [];
[LoadRecord("EventAZXAppleGameCharacterTable.json", "Id")]
public readonly Dictionary<int, EventAZXAppleGameCharacterRecord_Raw> EventAZXAppleGameCharacterTable = [];
[LoadRecord("EventAZXAppleGameSkillTable.json", "Id")]
public readonly Dictionary<int, EventAZXAppleGameSkillRecord_Raw> EventAZXAppleGameSkillTable = [];
[LoadRecord("EventAZXAppleGameCutSceneTable.json", "Id")]
public readonly Dictionary<int, EventAZXAppleGameCutSceneRecord_Raw> EventAZXAppleGameCutSceneTable = [];
static async Task<GameData> BuildAsync()
{
await Load();
@@ -268,30 +366,18 @@ namespace EpinelPS.Data
return Instance;
}
public GameData(string filePath, string mpkFilePath)
public GameData(string mpkFilePath)
{
if (!File.Exists(filePath)) throw new ArgumentException("Static data file must exist", nameof(filePath));
if (!File.Exists(mpkFilePath)) throw new ArgumentException("Static data file must exist", nameof(mpkFilePath));
// disable warnings
ZipStream = new();
// process json data
byte[] rawBytes = File.ReadAllBytes(filePath);
Sha256Hash = SHA256.HashData(rawBytes);
Size = rawBytes.Length;
byte[] rawBytes2 = File.ReadAllBytes(mpkFilePath);
MpkHash = SHA256.HashData(rawBytes2);
MpkSize = rawBytes2.Length;
// process mpk data
if (!string.IsNullOrEmpty(mpkFilePath))
{
byte[] rawBytes2 = File.ReadAllBytes(mpkFilePath);
MpkHash = SHA256.HashData(rawBytes2);
MpkSize = rawBytes2.Length;
}
if (UseMemoryPack)
LoadGameData(mpkFilePath, GameConfig.Root.StaticDataMpk);
else
LoadGameData(filePath, GameConfig.Root.StaticData);
LoadGameData(mpkFilePath, GameConfig.Root.StaticDataMpk);
if (MainZip == null) throw new Exception("failed to read zip file");
}
@@ -307,8 +393,9 @@ namespace EpinelPS.Data
{
using FileStream fileStream = File.Open(file, FileMode.Open, FileAccess.Read);
Rfc2898DeriveBytes a = new(PresharedValue, data.GetSalt2Bytes(), 10000, HashAlgorithmName.SHA256);
byte[] key2 = a.GetBytes(32);
// Rfc2898DeriveBytes a = new(PresharedValue, data.GetSalt2Bytes(), 10000, HashAlgorithmName.SHA256);
// byte[] key2 = a.GetBytes(32);
byte[] key2 = Rfc2898DeriveBytes.Pbkdf2(PresharedValue, data.GetSalt2Bytes(), 10000, HashAlgorithmName.SHA256, 32);
byte[] decryptionKey = key2[0..16];
byte[] iv = key2[16..32];
@@ -344,8 +431,9 @@ namespace EpinelPS.Data
throw new Exception("error 3");
dataMs.Position = 0;
Rfc2898DeriveBytes keyDec2 = new(PresharedValue, data.GetSalt1Bytes(), 10000, HashAlgorithmName.SHA256);
byte[] key3 = keyDec2.GetBytes(32);
// Rfc2898DeriveBytes keyDec2 = new(PresharedValue, data.GetSalt1Bytes(), 10000, HashAlgorithmName.SHA256);
// byte[] key3 = keyDec2.GetBytes(32);
byte[] key3 = Rfc2898DeriveBytes.Pbkdf2(PresharedValue, data.GetSalt1Bytes(), 10000, HashAlgorithmName.SHA256, 32);
byte[] val2 = key3[0..16];
byte[] iv2 = key3[16..32];
@@ -411,15 +499,8 @@ namespace EpinelPS.Data
public static async Task Load()
{
string? targetFile = await AssetDownloadUtil.DownloadOrGetFileAsync(GameConfig.Root.StaticData.Url, CancellationToken.None) ?? throw new Exception("static data download fail");
if (string.IsNullOrEmpty(GameConfig.Root.StaticDataMpk.Url))
{
_instance = new(targetFile, "");
return;
}
string? targetFile2 = await AssetDownloadUtil.DownloadOrGetFileAsync(GameConfig.Root.StaticDataMpk.Url, CancellationToken.None) ?? throw new Exception("static data download fail");
_instance = new(targetFile, targetFile2);
_instance = new(targetFile2);
}
#endregion
@@ -427,7 +508,7 @@ namespace EpinelPS.Data
{
try
{
if (UseMemoryPack) entry = entry.Replace(".json", ".mpk");
entry = entry.Replace(".json", ".mpk");
ZipEntry fileEntry = MainZip.GetEntry(entry);
if (fileEntry == null)
@@ -436,22 +517,8 @@ namespace EpinelPS.Data
return [];
}
X[]? deserializedObject;
if (UseMemoryPack)
{
Stream stream = MainZip.GetInputStream(fileEntry);
deserializedObject = await MemoryPackSerializer.DeserializeAsync<X[]>(stream);
}
else
{
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];
}
if (deserializedObject == null) throw new Exception("failed to parse " + entry);
Stream stream = MainZip.GetInputStream(fileEntry);
X[] deserializedObject = await MemoryPackSerializer.DeserializeAsync<X[]>(stream) ?? throw new Exception("failed to parse " + entry);
currentFile++;
bar.Report((double)currentFile / totalFiles);
@@ -499,7 +566,7 @@ namespace EpinelPS.Data
if (QuestDataRecords.Count == 0) throw new Exception("QuestDataRecords should not be empty");
foreach (KeyValuePair<int, MainQuestRecord> item in QuestDataRecords)
{
if (item.Value.ConditionId == stage)
if (item.Value.ConditionId[0].ConditionId == stage)
{
return item.Value;
}
@@ -558,45 +625,6 @@ namespace EpinelPS.Data
}
return -1;
}
public string? GetMapIdFromDBFieldName(string field)
{
// Get game map ID from DB Field Name (ex: 1_Normal for chapter 1 normal)
string[] keys = field.Split("_");
if (int.TryParse(keys[0], out int chapterNum))
{
string difficulty = keys[1];
foreach (KeyValuePair<int, CampaignChapterRecord> item in ChapterCampaignData)
{
if (difficulty == "Normal" && item.Value.Chapter == chapterNum)
{
return item.Value.FieldId;
}
else if (difficulty == "Hard" && item.Value.Chapter == chapterNum)
{
return item.Value.HardFieldId;
}
}
return null;
}
else
{
return keys[0]; // Already a Map ID
}
}
public int GetNormalChapterNumberFromFieldName(string field)
{
foreach (KeyValuePair<int, CampaignChapterRecord> item in ChapterCampaignData)
{
if (item.Value.FieldId == field)
{
return item.Value.Chapter;
}
}
return -1;
}
public IEnumerable<int> GetAllCostumes()
{
foreach (KeyValuePair<int, CharacterCostumeRecord> item in CharacterCostumeTable)
@@ -667,45 +695,32 @@ namespace EpinelPS.Data
}
// Example regular stage format: "d_main_26_08"
// Example bonus stage format: "d_main_18af_06"
// Example bonus stage format: "d_main_18af_06" or "d_main_39_af_01" (since chapter 39)
// Example stage with suffix format: "d_main_01_01_s" or "d_main_01_01_e"
string[] parts = scenarioGroupId.Split('_');
if (parts.Length < 4)
var matches = Regex.Matches(scenarioGroupId, @"\d+");
var parts = new List<int>();
foreach (Match match in matches)
{
return false; // If it doesn't have at least 4 parts, it's not a valId stage
if (int.TryParse(match.Value, out int number))
{
parts.Add(number);
}
}
if (parts.Count < 2) // Valid stage must have at least chapter and stage numbers
{
return false;
}
string chapterPart = parts[2]; // This could be "26", "18af", "01"
string stagePart = parts[3]; // This is the stage part, e.g., "08", "01_s", or "01_e"
int chapter = parts[0];
int stage = parts[1];
// Remove any suffixes like "_s", "_e" from the stage part for comparison
string cleanedStagePart = stagePart.Split('_')[0]; // Removes "_s", "_e", etc.
// Handle bonus stages (ending in "af" or having "_s", "_e" suffix)
bool isBonusStage = chapterPart.EndsWith("af") || stagePart.Contains("_s") || stagePart.Contains("_e");
// Extract chapter number (remove "af" if present)
string chapterNumberStr = isBonusStage && chapterPart.EndsWith("af")
? chapterPart[..^2] // Remove "af"
: chapterPart;
// Parse chapter and stage numbers
if (int.TryParse(chapterNumberStr, out int chapter) && int.TryParse(cleanedStagePart, out int stage))
// Only accept stages if they are:
// 1. In a chapter less than the target chapter
// 2. OR in the target chapter but with a stage number less than or equal to the target stage
if (chapter < targetChapter || (chapter == targetChapter && (stage <= targetStage)))
{
// Check if it's a bonus stage with a suffix
bool isSpecialStage = stagePart.Contains("_s") || stagePart.Contains("_e");
// Only accept stages if they are:
// 1. In a chapter less than the target chapter
// 2. OR in the target chapter but with a stage number less than or equal to the target stage
// 3. OR it's a special stage (with "_s" or "_e") in the target chapter and target stage
if (chapter < targetChapter ||
(chapter == targetChapter && (stage < targetStage || (stage == targetStage && isSpecialStage))))
{
return true;
}
return true;
}
return false;
@@ -716,19 +731,17 @@ namespace EpinelPS.Data
CampaignChapterRecord data = ChapterCampaignData[chapter - 1];
if (mod == ChapterMod.Hard)
return data.HardFieldId;
else return data.FieldId;
}
internal string GetMapIdFromChapter(int chapter, string mod)
{
CampaignChapterRecord data = ChapterCampaignData[chapter - 1];
if (mod == "Hard")
return data.HardFieldId;
else return data.FieldId;
else if (mod == ChapterMod.Normal)
return data.FieldId;
else if (mod == ChapterMod.Story)
return data.StoryFieldId;
throw new NotImplementedException($"difficulty {mod} not implemented");
}
internal int GetConditionReward(int groupId, long damage)
{
IEnumerable<KeyValuePair<int, ConditionRewardRecord>> results = ConditionRewards.Where(x => x.Value.Group == groupId && x.Value.ValueMin <= damage && x.Value.ValueMax >= damage);
IEnumerable<KeyValuePair<int, ConditionRewardRecord>> results = ConditionRewards.Where(x => x.Value.Group == groupId && x.Value.ValueMin <= damage && (x.Value.ValueMax == 0 || x.Value.ValueMax >= damage));
if (results.Any())
return results.FirstOrDefault().Value.RewardId;
else return 0;

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IncludeHttpRuleProtos>true</IncludeHttpRuleProtos>
@@ -10,7 +10,7 @@
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>True</IncludeNativeLibrariesForSelfExtract>
<NoWarn>$(NoWarn);SYSLIB0057</NoWarn>
<Version>0.135.4.3</Version>
<Version>0.140.8.0</Version>
<CETCompat>false</CETCompat>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<ImplicitUsings>enable</ImplicitUsings>
@@ -22,11 +22,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ASodium" Version="0.6.2" />
<PackageReference Include="ASodium" Version="0.6.4" />
<PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" />
<PackageReference Include="Grpc.Tools" Version="2.72.0">
<PackageReference Include="Google.Protobuf.Tools" Version="3.33.1" />
<PackageReference Include="Grpc.Tools" Version="2.76.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -36,8 +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="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="log4net" Version="3.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -10,8 +10,8 @@ namespace EpinelPS.LobbyServer.Archive
ReqClearArchiveStage req = await ReadData<ReqClearArchiveStage>(); // has fields EventId, StageId, BattleResult
int evid = req.EventId;
int stgid = req.StageId;
int result = req.BattleResult; // if 2 add to event info as last stage
User user = GetUser() ?? throw new Exception("User not found.");
int result = req.BattleResult;
User user = GetUser();
// Check if the EventInfo exists for the given EventId
if (!user.EventInfo.TryGetValue(evid, out EventData? eventData))
@@ -19,13 +19,12 @@ namespace EpinelPS.LobbyServer.Archive
throw new Exception($"Event with ID {evid} not found.");
}
// Update the EventData if BattleResult is 2
if (result == 1)
// Update the EventData if BattleResult is 1
if (result == 1 && !eventData.ClearedStages.Contains(stgid))
{
eventData.ClearedStages.Add(stgid);
// Update the LastStage in EventData
eventData.LastStage = stgid;
}
JsonDb.Save();
ResClearArchiveStage response = new();

View File

@@ -11,16 +11,19 @@ namespace EpinelPS.LobbyServer.Archive
int evid = req.EventId;
int stgid = req.StageId;
User user = GetUser() ?? throw new Exception("User not found.");
User user = GetUser();
// Check if the EventInfo exists for the given EventId
if (!user.EventInfo.TryGetValue(evid, out EventData? eventData))
{
throw new Exception($"Event with ID {evid} not found.");
}
// Update the LastStage in EventData
eventData.LastStage = stgid;
if (!eventData.ClearedStages.Contains(stgid))
{
eventData.ClearedStages.Add(stgid);
// Update the LastStage in EventData
eventData.LastStage = stgid;
}
JsonDb.Save();
ResFastClearArchiveStage response = new();

View File

@@ -1,7 +1,4 @@
using EpinelPS.Utils;
using EpinelPS.Data; // Ensure this namespace is included
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/scenario/getnonresettable")]
@@ -9,41 +6,16 @@ namespace EpinelPS.LobbyServer.Archive
{
protected override async Task HandleAsync()
{
ReqGetNonResettableArchiveScenario req = await ReadData<ReqGetNonResettableArchiveScenario>(); // req has EventId field
int evId = req.EventId;
ReqGetNonResettableArchiveScenario req = await ReadData<ReqGetNonResettableArchiveScenario>();
ResGetNonResettableArchiveScenario response = new();
// Access the GameData instance
GameData gameData = GameData.Instance;
if (evId == 130002)
User user = GetUser();
foreach (var (evtId, evtData) in user.EventInfo)
{
// Directly use the archiveEventQuestRecords dictionary
foreach (ArchiveEventQuestRecord_Raw record in gameData.archiveEventQuestRecords.Values)
if (evtId == req.EventId)
{
if (record.EventQuestManagerId == evId)
{
// Add the end_scenario_Id to the ScenarioIdList
if (!string.IsNullOrEmpty(record.EndScenarioId))
{
response.ScenarioIdList.Add(record.EndScenarioId);
}
}
}
}
else
{
// Directly use the archiveEventStoryRecords dictionary
foreach (ArchiveEventStoryRecord record in gameData.archiveEventStoryRecords.Values)
{
if (record.EventId == evId)
{
// Add the PrologueScenario to the ScenarioIdList
if (!string.IsNullOrEmpty(record.PrologueScenario))
{
response.ScenarioIdList.Add(record.PrologueScenario);
}
}
response.ScenarioIdList.AddRange(evtData.CompletedScenarios);
break;
}
}

View File

@@ -10,11 +10,21 @@ namespace EpinelPS.LobbyServer.Archive
ReqGetResettableArchiveScenario req = await ReadData<ReqGetResettableArchiveScenario>();
ResGetResettableArchiveScenario response = new(); // has ScenarioIdList field that takes in strings
// Retrieve stage IDs from GameData
List<string> stageIds = [.. GameData.Instance.archiveEventDungeonStageRecords.Values.Select(record => record.StageId.ToString())];
// Add them to the response
response.ScenarioIdList.Add(stageIds);
GameData gameData = GameData.Instance;
User user = GetUser();
foreach (ArchiveEventStoryRecord record in gameData.archiveEventStoryRecords.Values)
{
// Add the PrologueScenario to the ScenarioIdList
if (record.EventId == req.EventId && !string.IsNullOrEmpty(record.PrologueScenario))
{
if (user.EventInfo.TryGetValue(req.EventId, out EventData? evtData) &&
evtData.CompletedScenarios.Contains(record.PrologueScenario))
{
response.ScenarioIdList.Add(record.PrologueScenario);
}
break;
}
}
await WriteDataAsync(response);
}

View File

@@ -0,0 +1,21 @@
using EpinelPS.Utils;
using Google.Protobuf;
namespace EpinelPS.LobbyServer.Badge
{
[PacketPath("/badge/permanentcontent")]
public class PermanentContent : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqPermanentContentBadgeData req = await ReadData<ReqPermanentContentBadgeData>();
User user = GetUser();
ResPermanentContentBadgeData response = new();
// TODO
await WriteDataAsync(response);
}
}
}

View File

@@ -20,7 +20,7 @@ namespace EpinelPS.LobbyServer.Campaign
response.FieldObjectItemsNum.Add(new NetCampaignFieldObjectItemsNum()
{
MapId = map.Key,
Count = map.Value.CompletedObjects.Count
Count = map.Value.CompletedObjects.Where(x => x.Type == 1).Count()
});
}

View File

@@ -22,7 +22,7 @@ namespace EpinelPS.LobbyServer.Character
}
// TODO: ValIdate response from real server and pull info from user info
// TODO: Validate response from real server and pull info from user info
await WriteDataAsync(response);
}
}

View File

@@ -13,7 +13,27 @@ namespace EpinelPS.LobbyServer.Character
ResGetCharacterData response = new();
foreach (CharacterModel item in user.Characters)
{
response.Character.Add(new NetUserCharacterData() { Default = new() { Csn = item.Csn, Skill1Lv = item.Skill1Lvl, Skill2Lv = item.Skill2Lvl, CostumeId = item.CostumeId, Lv = user.GetCharacterLevel(item.Csn, item.Level), Grade = item.Grade, Tid = item.Tid, UltiSkillLv = item.UltimateLevel }, IsSynchro = user.GetSynchro(item.Csn) });
response.Character.Add(new NetUserCharacterData()
{
Default = new()
{
Csn = item.Csn,
Skill1Lv = item.Skill1Lvl,
Skill2Lv = item.Skill2Lvl,
CostumeId = item.CostumeId,
Lv = user.GetCharacterLevel(item.Csn, item.Level),
Grade = item.Grade,
Tid = item.Tid,
UltiSkillLv = item.UltimateLevel
},
IsSynchro = user.GetSynchro(item.Csn)
});
// Check if character is main force
if (item.IsMainForce)
{
response.MainForceCsnList.Add(item.Csn);
}
}
List<CharacterModel> highestLevelCharacters = [.. user.Characters.OrderByDescending(x => x.Level).Take(5)];

View File

@@ -0,0 +1,29 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/mainforce/set")]
public class SetCharacterMainForce : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSetCharacterMainForce req = await ReadData<ReqSetCharacterMainForce>();
User user = GetUser();
foreach (CharacterModel item in user.Characters)
{
if (item.Csn == req.Csn)
{
item.IsMainForce = req.IsMainForce;
break;
}
}
JsonDb.Save();
ResSetCharacterMainForce response = new();
await WriteDataAsync(response);
}
}
}

View File

@@ -8,7 +8,7 @@ namespace EpinelPS.LobbyServer.Character
{
protected override async Task HandleAsync()
{
// Broken protocol so we dIdn't valIdate request data.
// Broken protocol so we dIdn't validate request data.
// May fix later.
ReqSynchroAddSlot req = await ReadData<ReqSynchroAddSlot>();

View File

@@ -31,7 +31,7 @@ namespace EpinelPS.LobbyServer.ContentsOpen
JsonDb.Save();
}
foreach (KeyValuePair<int, UnlockData> item in user.ContentsOpenUnlocked)
foreach (KeyValuePair<int, UnlockData> item in user.ContentsOpenUnlocked.OrderBy(x => x.Key))
{
response.ContentsOpenUnlockInfoList.Add(new NetContentsOpenUnlockInfo()
{

View File

@@ -13,7 +13,10 @@ namespace EpinelPS.LobbyServer.Event
if (user.EventInfo.TryGetValue(req.EventId, out EventData? evt))
{
evt.CompletedScenarios.Add(req.ScenarioId);
if (!evt.CompletedScenarios.Contains(req.ScenarioId))
{
evt.CompletedScenarios.Add(req.ScenarioId);
}
}
else
{
@@ -21,7 +24,8 @@ namespace EpinelPS.LobbyServer.Event
evt.CompletedScenarios.Add(req.ScenarioId);
user.EventInfo.Add(req.EventId, evt);
}
JsonDb.Save();
ResSetEventScenarioComplete response = new();
// TODO reward

View File

@@ -1,5 +1,4 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
@@ -13,14 +12,12 @@ namespace EpinelPS.LobbyServer.Event
ResEnterEventField response = new()
{
Field = new(),
Json = "{}"
Field = new()
};
// Retrieve collected objects
// Retrieve collected objects and completed stages
if (!user.FieldInfoNew.TryGetValue(req.MapId, out FieldInfoNew? field))
{
field = new FieldInfoNew();
@@ -33,7 +30,9 @@ namespace EpinelPS.LobbyServer.Event
}
foreach (NetFieldObject obj in field.CompletedObjects)
{
response.Field.Objects.Add(obj);
if (obj == null) continue;
if (obj.Type == 1)
response.Field.Objects.Add(obj);
}

View File

@@ -0,0 +1,292 @@
using EpinelPS.Data;
using EpinelPS.Utils;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Event
{
public class EventHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(EventHelper));
public static void AddEvents(User user, ref ResGetEventList response)
{
List<LobbyPrivateBannerRecord> lobbyPrivateBanners = GetLobbyPrivateBannerData(user);
if (lobbyPrivateBanners.Count == 0)
{
// No active lobby private banners
Logging.WriteLine("No active lobby private banners found.", LogType.Warning);
return;
}
var eventManagers = GameData.Instance.eventManagers.Values.ToList();
foreach (var banner in lobbyPrivateBanners)
{
// Get all events (including child events) associated with this banner
List<NetEventData> events = GetEventData(banner, eventManagers);
log.Debug($"Banner EventId: {banner.EventId} has {events.Count} associated events: {JsonConvert.SerializeObject(events)}");
AddEvents(ref response, events);
// Additionally, get any gacha events associated with this banner
List<EventSystemType> systemTypes = [EventSystemType.PickupGachaEvent, EventSystemType.BoxGachaEvent, EventSystemType.LoginEvent];
List<NetEventData> gachaEvents = GetEventDataBySystemTypes(banner, eventManagers, systemTypes);
log.Debug($"Banner EventId: {banner.EventId} has {gachaEvents.Count} associated gacha events: {JsonConvert.SerializeObject(gachaEvents)}");
AddEvents(ref response, gachaEvents);
// add challenge events
var challengeEvents = GetChallengeEventData(banner, eventManagers);
log.Debug($"Banner EventId: {banner.EventId} has {challengeEvents.Count} associated challenge events: {JsonConvert.SerializeObject(challengeEvents)}");
AddEvents(ref response, challengeEvents);
}
// add daily mission events
List<NetEventData> dailyMissionEvents = GetDailyMissionEventData(eventManagers);
log.Debug($"Found {dailyMissionEvents.Count} associated daily mission events: {JsonConvert.SerializeObject(dailyMissionEvents)}");
AddEvents(ref response, dailyMissionEvents);
}
public static void AddJoinedEvents(User user, ref ResGetJoinedEvent response)
{
List<LobbyPrivateBannerRecord> lobbyPrivateBanners = GetLobbyPrivateBannerData(user);
if (lobbyPrivateBanners.Count == 0)
{
// No active lobby private banners
Logging.WriteLine("No active lobby private banners found.", LogType.Warning);
return;
}
var eventManagers = GameData.Instance.eventManagers.Values.ToList();
foreach (var banner in lobbyPrivateBanners)
{
// Get all events (including child events) associated with this banner
var events = GetEventData(banner, eventManagers);
log.Debug($"Banner EventId: {banner.EventId} has {events.Count} associated events: {JsonConvert.SerializeObject(events)}");
AddJoinedEvents(ref response, events);
// add gacha events
List<EventSystemType> systemTypes = [EventSystemType.PickupGachaEvent, EventSystemType.BoxGachaEvent, EventSystemType.LoginEvent];
List<NetEventData> gachaEvents = GetEventDataBySystemTypes(banner, eventManagers, systemTypes);
log.Debug($"Banner EventId: {banner.EventId} has {gachaEvents.Count} associated gacha events: {JsonConvert.SerializeObject(gachaEvents)}");
AddJoinedEvents(ref response, gachaEvents);
// add challenge events
List<NetEventData> challengeEvents = GetChallengeEventData(banner, eventManagers);
log.Debug($"Banner EventId: {banner.EventId} has {challengeEvents.Count} associated challenge events: {JsonConvert.SerializeObject(challengeEvents)}");
AddJoinedEvents(ref response, challengeEvents);
}
// add daily mission events
List<NetEventData> dailyMissionEvents = GetDailyMissionEventData(eventManagers);
log.Debug($"Found {dailyMissionEvents.Count} associated daily mission events: {JsonConvert.SerializeObject(dailyMissionEvents)}");
AddJoinedEvents(ref response, dailyMissionEvents);
}
private static List<NetEventData> GetEventData(LobbyPrivateBannerRecord banner, List<EventManagerRecord> eventManagers)
{
List<NetEventData> events = [];
if (!eventManagers.Any(em => em.Id == banner.EventId))
{
Logging.WriteLine($"No event manager found for Banner EventId: {banner.EventId}", LogType.Warning);
return events;
}
// Add the main event associated with the banner
var mainEvent = eventManagers.First(em => em.Id == banner.EventId);
events.Add(new NetEventData()
{
Id = mainEvent.Id,
EventSystemType = (int)mainEvent.EventSystemType,
// EventStartDate = banner.StartDate.Ticks,
// EventVisibleDate = banner.StartDate.Ticks,
// EventDisableDate = banner.EndDate.Ticks,
// EventEndDate = banner.EndDate.Ticks
});
// Add child events associated with the main event
var childEvents = eventManagers.Where(em => em.ParentsEventId == banner.EventId || em.SetField == banner.EventId).ToList();
foreach (var childEvent in childEvents)
{
events.Add(new NetEventData()
{
Id = childEvent.Id,
EventSystemType = (int)childEvent.EventSystemType,
// EventStartDate = banner.StartDate.Ticks,
// EventVisibleDate = banner.StartDate.Ticks,
// EventDisableDate = banner.EndDate.Ticks,
// EventEndDate = banner.EndDate.Ticks
});
}
return events;
}
private static List<NetEventData> GetEventDataBySystemTypes(LobbyPrivateBannerRecord banner, List<EventManagerRecord> eventManagers, List<EventSystemType> systemTypes)
{
List<NetEventData> events = [];
// Find all event banner resource tables associated with this banner's EventId
List<string> eventBannerResourceTables = [.. eventManagers.Where(em =>
(em.SetField == banner.EventId || em.ParentsEventId == banner.EventId)
&& em.EventBannerResourceTable.StartsWith("event_")).Select(em => em.EventBannerResourceTable)];
eventBannerResourceTables = [.. eventBannerResourceTables.Distinct()];
log.Debug($"Banner EventId: {banner.EventId} has {eventBannerResourceTables.Count} associated event banner resource tables: {JsonConvert.SerializeObject(eventBannerResourceTables)}");
if (eventBannerResourceTables.Count == 0)
{
Logging.WriteLine($"No associated event banner resource tables found for Banner EventId: {banner.EventId}", LogType.Warning);
return events;
}
// Find all events matching the banner resource tables and specified system types
var gachaEvents = eventManagers.Where(em =>
eventBannerResourceTables.Contains(em.EventBannerResourceTable)
&& systemTypes.Contains(em.EventSystemType)).ToList();
log.Debug($"Found {gachaEvents.Count} gacha events from banner resource tables: {JsonConvert.SerializeObject(gachaEvents)}");
if (gachaEvents.Count == 0)
{
Logging.WriteLine($"No gacha events found for Banner EventId: {banner.EventId}", LogType.Warning);
return events;
}
// Add each gacha event to the list
foreach (var gachaEvent in gachaEvents)
{
events.Add(new NetEventData()
{
Id = gachaEvent.Id,
EventSystemType = (int)gachaEvent.EventSystemType,
// EventStartDate = banner.StartDate.Ticks,
// EventVisibleDate = banner.StartDate.Ticks,
// EventDisableDate = banner.EndDate.Ticks,
// EventEndDate = banner.EndDate.Ticks
});
}
return events;
}
private static List<NetEventData> GetChallengeEventData(LobbyPrivateBannerRecord banner, List<EventManagerRecord> eventManagers)
{
List<NetEventData> events = [];
// Find all challenge events (ChallengeModeEvent) associated with this banner's EventId
var challengeEvents = eventManagers.Where(em =>
em.ParentsEventId == banner.EventId && em.EventSystemType == EventSystemType.ChallengeModeEvent).ToList();
log.Debug($"Found {challengeEvents.Count} challenge events from banner resource tables: {JsonConvert.SerializeObject(challengeEvents)}");
if (challengeEvents.Count == 0)
{
Logging.WriteLine($"No challenge events found for Banner EventId: {banner.EventId}", LogType.Warning);
return events;
}
// Add each challenge event to the list
foreach (var challengeEvent in challengeEvents)
{
events.Add(new NetEventData()
{
Id = challengeEvent.Id,
EventSystemType = (int)challengeEvent.EventSystemType,
// EventStartDate = banner.StartDate.Ticks,
// EventVisibleDate = banner.StartDate.Ticks,
// EventDisableDate = banner.EndDate.Ticks,
// EventEndDate = banner.EndDate.Ticks
});
}
return events;
}
/// <summary>
/// Get active lobby private banner data
/// </summary>
/// <returns>List of active lobby private banners</returns>
public static List<LobbyPrivateBannerRecord> GetLobbyPrivateBannerData(User user)
{
var lobbyPrivateBannerIds = user.LobbyPrivateBannerIds;
var lobbyPrivateBannerRecords = GameData.Instance.LobbyPrivateBannerTable.Values;
List<LobbyPrivateBannerRecord> lobbyPrivateBanners = [];
if (lobbyPrivateBannerIds is not null && lobbyPrivateBannerIds.Count > 0)
{
lobbyPrivateBanners = [.. lobbyPrivateBannerRecords.Where(b => lobbyPrivateBannerIds.Contains(b.Id))];
}
else
{
lobbyPrivateBanners.Add(lobbyPrivateBannerRecords.OrderBy(b => b.Id).Last());
}
Logging.WriteLine($"Found {lobbyPrivateBanners.Count} active lobby private banners.", LogType.Debug);
log.Debug($"Active lobby private banners: {JsonConvert.SerializeObject(lobbyPrivateBanners)}");
return lobbyPrivateBanners;
}
private static void AddEvents(ref ResGetEventList response, List<NetEventData> eventDatas)
{
foreach (var eventData in eventDatas)
{
// if (eventData.Id == 70115) continue;
// Avoid adding duplicate events
if (!response.EventList.Any(e => e.Id == eventData.Id))
{
if (eventData.EventStartDate == 0) eventData.EventStartDate = DateTime.UtcNow.AddDays(-1).Ticks;
if (eventData.EventVisibleDate == 0) eventData.EventVisibleDate = DateTime.UtcNow.AddDays(-1).Ticks;
if (eventData.EventDisableDate == 0) eventData.EventDisableDate = DateTime.UtcNow.AddDays(30).Ticks;
if (eventData.EventEndDate == 0) eventData.EventEndDate = DateTime.UtcNow.AddDays(30).Ticks;
response.EventList.Add(eventData);
}
else
{
log.Debug($"Skipping duplicate event Id: {eventData.Id}");
}
}
}
private static void AddJoinedEvents(ref ResGetJoinedEvent response, List<NetEventData> eventDatas)
{
foreach (var eventData in eventDatas)
{
if (eventData.Id == 70115) continue;
// Avoid adding duplicate events
if (!response.EventWithJoinData.Any(e => e.EventData.Id == eventData.Id))
{
if (eventData.EventStartDate == 0) eventData.EventStartDate = DateTime.UtcNow.AddDays(-1).Ticks;
if (eventData.EventVisibleDate == 0) eventData.EventVisibleDate = DateTime.UtcNow.AddDays(-1).Ticks;
if (eventData.EventDisableDate == 0) eventData.EventDisableDate = DateTime.UtcNow.AddDays(30).Ticks;
if (eventData.EventEndDate == 0) eventData.EventEndDate = DateTime.UtcNow.AddDays(30).Ticks;
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = eventData,
JoinAt = 0
});
}
else
{
log.Debug($"Skipping duplicate event Id: {eventData.Id}");
}
}
}
private static List<NetEventData> GetDailyMissionEventData(List<EventManagerRecord> eventManagers)
{
List<NetEventData> events = [];
var dailyEventIds = GameData.Instance.DailyMissionEventSettingTable.Values.Select(de => de.EventId).ToList();
log.Debug($"Daily Mission Event IDs: {JsonConvert.SerializeObject(dailyEventIds)}");
var dailyEvents = eventManagers.Where(em => dailyEventIds.Contains(em.Id)).ToList();
log.Debug($"Found {dailyEvents.Count} daily events: {JsonConvert.SerializeObject(dailyEvents)}");
if (dailyEvents.Count == 0)
{
Logging.WriteLine("No daily events found.", LogType.Warning);
return events;
}
// Add each daily event to the list
foreach (var dailyEvent in dailyEvents)
{
events.Add(new NetEventData()
{
Id = dailyEvent.Id,
EventSystemType = (int)dailyEvent.EventSystemType,
EventStartDate = DateTime.UtcNow.Ticks,
EventVisibleDate = DateTime.UtcNow.Ticks,
EventDisableDate = DateTime.UtcNow.AddDays(30).Ticks,
EventEndDate = DateTime.UtcNow.AddDays(30).Ticks
});
}
return events;
}
}
}

View File

@@ -1,3 +1,4 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
@@ -9,32 +10,32 @@ namespace EpinelPS.LobbyServer.Event
protected override async Task HandleAsync()
{
ReqLoginEventData req = await ReadData<ReqLoginEventData>();
User user = GetUser();
int evId = req.EventId;
ResLoginEventData response = new()
{
EndDate = DateTime.Now.AddDays(13).Ticks,
DisableDate = DateTime.Now.AddDays(13).Ticks,
LastAttendance = new LoginEventAttendance
{
Day = 14, // Example day value, adjust as needed
AttendanceDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks // Assign Ticks here
}
LastAttendance = new LoginEventAttendance()
}; // fields "EndDate", "DisableDate", "RewardHistories", "LastAttendance"
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 1 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 2 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 3 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 4 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 5 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 6 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 7 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 8 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 9 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 10 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 11 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 12 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 13 } );
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = true, Day = 14 } );
// Check if event exists
if (!user.LoginEventInfo.TryGetValue(evId, out var loginEventData))
{
loginEventData = new LoginEventData();
user.LoginEventInfo.Add(evId, loginEventData);
JsonDb.Save();
}
// Populate response with event data
int day = 1;
GameData.Instance.LoginEventTable.Values.Where(ev => ev.EventId == evId).ToList().ForEach(ev =>
{
loginEventData.LastDay = day++;
response.RewardHistories.Add(new LoginEventRewardHistory() { IsReceived = loginEventData.Days.Contains(ev.Day), Day = ev.Day });
});
response.LastAttendance.Day = loginEventData.LastDay;
response.LastAttendance.AttendanceDate = loginEventData.LastDate;
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,38 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/login/receive")]
public class EventLoginReceive : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqObtainLoginEventReward Fields: EventId, Day
ReqObtainLoginEventReward req = await ReadData<ReqObtainLoginEventReward>();
User user = GetUser();
ResObtainLoginEventReward response = new();
if (!user.LoginEventInfo.TryGetValue(req.EventId, out var loginEventData))
{
loginEventData = new LoginEventData();
user.LoginEventInfo.Add(req.EventId, loginEventData);
}
GameData.Instance.LoginEventTable.Values.Where(ev => ev.EventId == req.EventId && ev.Day == req.Day).ToList().ForEach(ev =>
{
response.Reward = RewardUtils.RegisterRewardsForUser(user, ev.RewardId);
});
if (!loginEventData.Days.Contains(req.Day))
{
// loginEventData.Days.Add(req.Day);
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,46 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/login/allreceive")]
public class EventLoginReceiveAll : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqObtainLoginEventReward Fields: EventId, Day
ReqObtainAllLoginEventReward req = await ReadData<ReqObtainAllLoginEventReward>();
User user = GetUser();
ResObtainAllLoginEventReward response = new();
if (!user.LoginEventInfo.TryGetValue(req.EventId, out var loginEventData))
{
loginEventData = new LoginEventData();
user.LoginEventInfo.Add(req.EventId, loginEventData);
}
response.Reward = new();
NetRewardData rewardData = new();
GameData.Instance.LoginEventTable.Values.Where(ev => ev.EventId == req.EventId).ToList().ForEach(ev =>
{
if (!loginEventData.Days.Contains(ev.Day))
{
loginEventData.Days.Add(ev.Day);
RewardRecord reward = GameData.Instance.GetRewardTableEntry(ev.RewardId) ?? throw new Exception($"unknown reward Id {ev.RewardId}");
foreach (var item in reward.Rewards)
{
if (item.RewardType != RewardType.None)
{
RewardUtils.AddSingleObject(user, ref rewardData, item.RewardId, item.RewardType, item.RewardValue);
}
}
}
});
response.Reward = rewardData;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,22 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/boxgacha/execute")]
public class ExecuteEventBoxGacha : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// from client: {"EventId":10051,"CurrentCount":1}
ReqExecuteEventBoxGacha req = await ReadData<ReqExecuteEventBoxGacha>();
User user = GetUser();
ResExecuteEventBoxGacha response = new()
{
};
await WriteDataAsync(response);
}
}
}

View File

@@ -1,3 +1,5 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
@@ -13,11 +15,30 @@ namespace EpinelPS.LobbyServer.Event
ResChallengeEventStageData response = new()
{
RemainTicket = 3,
TeamData = user.UserTeams[1]
TeamData = new NetUserTeamData
{
Type = (int)TeamType.StoryEvent
},
};
// check if user has a team for this type
if (user.UserTeams.TryGetValue((int)TeamType.StoryEvent, out NetUserTeamData? teamData))
{
response.TeamData = teamData;
}
if (!user.EventInfo.TryGetValue(req.EventId, out EventData? eventData))
{
eventData = new() { LastStage = 0 };
user.EventInfo.Add(req.EventId, eventData);
}
// placeholder response data for last cleared stage
response.LastClearedEventStageList.Add(new NetLastClearedEventStageData()
{
DifficultyId = eventData.Diff,
StageId = eventData.LastStage
});
// TODO implement properly
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -1,20 +0,0 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/mission/getclear")]
public class GetClearedMissions : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetEventMissionClear req = await ReadData<ReqGetEventMissionClear>(); //has EventIdList
ResGetEventMissionClear response = new();
// response.ResGetEventMissionClear.Add(new NetEventMissionClearData(EventId = 0, EventMissionId = 0 , CreatedAt = 0));
// TODO
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,22 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/boxgacha/get")]
public class GetEventBoxGacha : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// from client: {"EventId":10051}
ReqGetEventBoxGacha req = await ReadData<ReqGetEventBoxGacha>();
User user = GetUser();
ResGetEventBoxGacha response = new()
{
};
await WriteDataAsync(response);
}
}
}

View File

@@ -1,7 +1,4 @@
using EpinelPS.Utils;
using EpinelPS.Data; // For GameData access
using System.Linq;
using System.Threading.Tasks;
namespace EpinelPS.LobbyServer.Event
{
@@ -15,15 +12,16 @@ namespace EpinelPS.LobbyServer.Event
ResGetEventScenarioData response = new();
/*
if (user.EventInfo.TryGetValue(req.EventId, out EventData? data))
if (response.ScenarioIdList.Count == 0)
{
response.ScenarioIdList.AddRange(data.CompletedScenarios);
if (user.EventInfo.TryGetValue(req.EventId, out EventData? data))
{
response.ScenarioIdList.AddRange(data.CompletedScenarios);
}
}
*/
// Get all scenario_group_Id values from albumResourceRecords starting with "event_"
response.ScenarioIdList.Add(GameData.Instance.albumResourceRecords.Values.Where(record => record.ScenarioGroupId.StartsWith("event_")).Select(record => record.ScenarioGroupId).ToList());
// response.ScenarioIdList.Add(GameData.Instance.albumResourceRecords.Values.Where(record => record.ScenarioGroupId.StartsWith("event_")).Select(record => record.ScenarioGroupId).ToList());
await WriteDataAsync(response);
}

View File

@@ -1,4 +1,5 @@
using EpinelPS.Utils;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/getjoinedevent")]
@@ -6,138 +7,14 @@ namespace EpinelPS.LobbyServer.Event
{
protected override async Task HandleAsync()
{
ReqGetJoinedEvent req = await ReadData<ReqGetJoinedEvent>();
await ReadData<ReqGetJoinedEvent>();
//types are defined in EventTypes.cs
ResGetJoinedEvent response = new();
User user = GetUser();
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 20001,
EventSystemType = (int)EventType.PickupGachaEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
// add gacha events from active lobby banners
EventHelper.AddJoinedEvents(user, ref response);
// ssr rapi
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 70077,
EventSystemType = (int)EventType.PickupGachaEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 10046,
EventSystemType = (int)EventType.LoginEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 40066,
EventSystemType = (int)EventType.StoryEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 60066,
EventSystemType = (int)EventType.ChallengeModeEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 70078,
EventSystemType = (int)EventType.PickupGachaEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 70079,
EventSystemType = (int)EventType.PickupGachaEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
//full burst day
/*
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 140052,
EventSystemType = RewardUpEvent,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
*/
//dailies reward up
/*
response.EventWithJoinData.Add(new NetEventWithJoinData()
{
EventData = new NetEventData()
{
Id = 170017,
EventSystemType = TriggerMissionEventReward,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks
},
JoinAt = 0
});
*/
await WriteDataAsync(response);
}
}

View File

@@ -1,5 +1,4 @@
using EpinelPS.Data;
using EpinelPS.Utils;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
@@ -12,98 +11,10 @@ namespace EpinelPS.LobbyServer.Event
// types are defined in EventTypes.cs
ResGetEventList response = new();
User user = GetUser();
// reborn evil collab event
response.EventList.Add(new NetEventData()
{
Id = 82600,
EventSystemType = (int)EventSystemType.FieldHubEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 82401,
EventSystemType = (int)EventSystemType.CE007MiniGame,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 82602,
EventSystemType = (int)EventSystemType.ShopEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 40090,
EventSystemType = (int)EventSystemType.StoryEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 40091,
EventSystemType = (int)EventSystemType.StoryEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 60090,
EventSystemType = (int)EventSystemType.ChallengeModeEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 100034,
EventSystemType = (int)EventSystemType.EventPass,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 100035,
EventSystemType = (int)EventSystemType.EventPass,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 82603,
EventSystemType = (int)EventSystemType.LoginEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
response.EventList.Add(new NetEventData()
{
Id = 30076,
EventSystemType = (int)EventSystemType.CooperationEvent,
EventVisibleDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)).Ticks,
EventStartDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)).Ticks,
EventEndDate = DateTime.Now.AddDays(20).Ticks,
EventDisableDate = DateTime.Now.AddDays(20).Ticks
});
// add events from active lobby banners
EventHelper.AddEvents(user, ref response);
await WriteDataAsync(response);
}

View File

@@ -0,0 +1,29 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/acquire/achievementmission/reward")]
public class AcquireAzxAchievementMissionReward : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqAcquireMiniGameAzxAchievementMissionReward Fields
// int AzxId
// RepeatedField<int> AchievementMissionIdList
ReqAcquireMiniGameAzxAchievementMissionReward req = await ReadData<ReqAcquireMiniGameAzxAchievementMissionReward>();
User user = GetUser();
// ResAcquireMiniGameAzxAchievementMissionReward Fields
// NetRewardData Reward
ResAcquireMiniGameAzxAchievementMissionReward response = new();
NetRewardData reward = new();
AzxHelper.AcquireReward(user, ref reward, req.AzxId, req.AchievementMissionIdList);
response.Reward = reward;
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,568 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
public static class AzxHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(AzxHelper));
public static void AcquireReward(User user, ref NetRewardData reward, int azxId, RepeatedField<int> missionIds)
{
log.Debug($"Acquiring reward for user {user.ID}, azxId: {azxId}, missionIds: {JsonConvert.SerializeObject(missionIds)}");
if (missionIds.Count == 0 || azxId == 0) return;
try
{
var missions = GameData.Instance.EventAZXAppleGameMissionTable.Values.Where(x =>
x.MissionRewardId > 0 && missionIds.Contains(x.Id)).ToList();
if (missions.Count == 0) return;
List<Reward_Data> rewardDatas = [];
foreach (var mission in missions)
{
var rewardRecord = GameData.Instance.GetRewardTableEntry(mission.MissionRewardId);
if (rewardRecord is null || rewardRecord.Rewards.Count == 0) continue;
foreach (var rewardItem in rewardRecord.Rewards)
{
if (rewardItem.RewardValue == 0) continue;
int itemIndex = rewardDatas.FindIndex(x => x.RewardId == rewardItem.RewardId);
if (itemIndex >= 0)
rewardDatas[itemIndex].RewardValue += rewardItem.RewardValue;
else
rewardDatas.Add(rewardItem);
}
}
foreach (var rewardData in rewardDatas)
{
RewardUtils.AddSingleObject(user, ref reward, rewardData.RewardId, rewardData.RewardType, rewardData.RewardValue);
}
var azxInfo = GetAzxInfo(user, azxId);
azxInfo.AchievementMissionDataList.AddRange(missionIds.Select(x =>
new AchievementMissionData() { MissionId = x, IsReceived = true }));
user.MiniGameAzxInfo[azxId] = azxInfo;
JsonDb.Save();
}
catch (Exception ex)
{
Logging.WriteLine($"Acquiring reward failed: {ex.Message}", LogType.Error);
}
}
public static void GetRanking(User user, int azxId, ref ResGetMiniGameAzxRanking response)
{
// ResGetMiniGameAzxRanking Fields
// NetMiniGameAzxRankingData UserGuildRanking
// RepeatedField<NetMiniGameAzxRankingData> GuildRankingList
// NetMiniGameAzxRankingData Fields
// int Rank
// NetMiniGameAzxScoreAndTime ScoreAndTime
// NetWholeUserData User
// NetMiniGameAzxScoreAndTime Fields
// int Score
// Google.Protobuf.WellKnownTypes.Duration TimeToScore
int dateDay = GetDateDay();
var azxInfo = GetAzxInfo(user, azxId);
var scoreData = azxInfo.ScoreDatas.Find(x => x.DateDay == dateDay && x.AzxId == azxId);
int score = scoreData?.HighScore ?? 0;
Duration timeToScore = scoreData?.HighScoreTime ?? new Duration() { Seconds = 0, Nanos = 0 };
response.UserGuildRanking = new NetMiniGameAzxRankingData()
{
Rank = 1,
ScoreAndTime = new NetMiniGameAzxScoreAndTime()
{
Score = score,
TimeToScore = timeToScore
},
User = new NetWholeUserData()
{
Usn = (long)user.ID,
Server = 10001,
Nickname = user.Nickname,
Lv = user.userPointData?.UserLevel ?? 99,
Icon = user.ProfileIconId,
IconPrism = user.ProfileIconIsPrism,
Frame = user.ProfileFrame,
LastActionAt = user.LastLogin.Ticks,
UserTitleId = user.TitleId,
GuildName = user.Nickname,
}
};
response.GuildRankingList.Add(new NetMiniGameAzxRankingData()
{
Rank = 2,
ScoreAndTime = new NetMiniGameAzxScoreAndTime()
{
Score = 80000,
TimeToScore = new Duration() { Seconds = 118, Nanos = 432877000 }
},
User = new NetWholeUserData()
{
Usn = (long)user.ID,
Server = 10001,
Nickname = user.Nickname,
Lv = user.userPointData?.UserLevel ?? 99,
Icon = user.ProfileIconId,
IconPrism = user.ProfileIconIsPrism,
Frame = user.ProfileFrame,
LastActionAt = user.LastLogin.Ticks,
UserTitleId = user.TitleId,
GuildName = user.Nickname,
}
});
JsonDb.Save();
}
public static void FinishAzx(User user, ReqFinishMiniGameAzx req, ref ResFinishMiniGameAzx response)
{
// ReqEnterMiniGameAzx Fields
// int AzxId
// NetMiniGameAzxScoreAndTime ScoreAndTime
// int PlayBoardId
// int PlayCharacterId
// RepeatedField<NetSkillUseCountData> SkillUseCountList
// int CutSceneId
// ResFinishMiniGameAzx Fields
// NetRewardData Reward
// NetMiniGameAzxDailyMissionData DailyMissionData
// bool IsNewHighScore
// bool IsNewHighScoreTime
try
{
log.Debug($"Finishing AZX for user {user.ID}, data: {JsonConvert.SerializeObject(req)}");
int dateDay = GetDateDay();
int score = req.ScoreAndTime.Score;
Duration timeToScore = req.ScoreAndTime.TimeToScore;
NetMiniGameAzxDailyMissionData dailyMissionData = new() { IsDailyRewarded = false, DailyAccumulatedScore = 0 };
NetRewardData reward = new();
var azxInfo = GetAzxInfo(user, req.AzxId);
if (req.CutSceneId > 0 && azxInfo.CutSceneDataList.Find(x => x.CutSceneId == req.CutSceneId) is null)
{
azxInfo.CutSceneDataList.Add(new CutSceneData() { CutSceneId = req.CutSceneId, IsNew = true });
}
var scoreDataIndex = azxInfo.ScoreDatas.FindIndex(x => x.DateDay == dateDay && x.AzxId == req.AzxId);
if (scoreDataIndex >= 0)
{
if (score > 10000 && !azxInfo.ScoreDatas[scoreDataIndex].IsDailyRewarded)
{
azxInfo.ScoreDatas[scoreDataIndex].IsDailyRewarded = true;
dailyMissionData.IsDailyRewarded = true;
RewardUtils.AddSingleCurrencyObject(user, ref reward, CurrencyType.FreeCash, 30);
}
azxInfo.ScoreDatas[scoreDataIndex].AccumulatedScore += score;
dailyMissionData.DailyAccumulatedScore = azxInfo.ScoreDatas[scoreDataIndex].AccumulatedScore;
if (azxInfo.ScoreDatas[scoreDataIndex].HighScore < score)
{
response.IsNewHighScore = true;
azxInfo.ScoreDatas[scoreDataIndex].HighScore = score;
}
if (azxInfo.ScoreDatas[scoreDataIndex].HighScoreTime.ToTimeSpan() > timeToScore.ToTimeSpan())
{
response.IsNewHighScoreTime = true;
azxInfo.ScoreDatas[scoreDataIndex].HighScoreTime = timeToScore;
}
}
else
{
bool isDailyRewarded = false;
if (score > 10000)
{
isDailyRewarded = true;
dailyMissionData.IsDailyRewarded = true;
RewardUtils.AddSingleCurrencyObject(user, ref reward, CurrencyType.FreeCash, 30);
}
response.IsNewHighScoreTime = true;
response.IsNewHighScore = true;
azxInfo.ScoreDatas.Add(new MiniGameAzxScoreData
{
AzxId = req.AzxId,
DateDay = dateDay,
AccumulatedScore = score,
HighScore = score,
HighScoreTime = timeToScore,
IsDailyRewarded = isDailyRewarded
});
dailyMissionData.DailyAccumulatedScore = score;
}
// int PlayBoardId
// int PlayCharacterId
azxInfo.CharacterCount ??= [];
if (azxInfo.CharacterCount.TryGetValue(req.PlayCharacterId, out var characterCount))
azxInfo.CharacterCount[req.PlayCharacterId] = characterCount + 1;
else
azxInfo.CharacterCount.Add(req.PlayCharacterId, 1);
// RepeatedField<NetSkillUseCountData> SkillUseCountList
if (req.SkillUseCountList != null && req.SkillUseCountList.Count > 0)
{
azxInfo.SkillCount ??= [];
foreach (var item in req.SkillUseCountList)
{
if (azxInfo.SkillCount.TryGetValue(item.SkillId, out var skillCount))
azxInfo.SkillCount[item.SkillId] = skillCount + item.SkillUseCount;
else
azxInfo.SkillCount.Add(item.SkillId, item.SkillUseCount);
}
}
response.DailyMissionData = dailyMissionData;
response.Reward = reward;
// Save changes
user.MiniGameAzxInfo[req.AzxId] = azxInfo;
JsonDb.Save();
}
catch (Exception ex)
{
Logging.WriteLine($"Finish AZX Error: {ex.Message}", LogType.Error);
}
}
public static void EnterAzx(User user, ref ResEnterMiniGameAzx response, int azxId, int playBoardId, int playCharacterId)
{
log.Debug($"Entering AZX AzxId: {azxId}, PlayBoardId: {playBoardId}, PlayCharacterId: {playCharacterId}");
try
{
var azxInfo = GetAzxInfo(user, azxId);
azxInfo.SelectedBoardId = playBoardId;
azxInfo.SelectedCharacterId = playCharacterId;
user.MiniGameAzxInfo[azxId] = azxInfo;
response.PreviousSRankCount = 0;
}
catch (Exception ex)
{
Logging.WriteLine($"Enter AZX Error: {ex.Message}", LogType.Error);
}
}
public static void GetAzxData(User user, int azxId, ref ResGetMiniGameAzxData response)
{
log.Debug($"Getting AZX data for user {user.ID}");
var azxInfo = GetAzxInfo(user, azxId);
// Initialize score data if null
azxInfo.ScoreDatas ??= [];
// Get sum score
int sumScore = azxInfo.ScoreDatas.Sum(x => x.AccumulatedScore);
log.Debug($"AZX data: {JsonConvert.SerializeObject(azxInfo)}");
try
{
// AchievementMissionDataList
var missions = GameData.Instance.EventAZXAppleGameMissionTable.Values.ToList();
foreach (var mission in missions)
{
bool isReceived = false;
azxInfo.AchievementMissionDataList ??= [];
var item = azxInfo.AchievementMissionDataList.Find(x => x.MissionId == mission.Id);
if (item is not null) isReceived = item.IsReceived;
if (mission.MissionType == EventAZXAppleGameMissionMissionType.GetScore)
{
AddAchievementMission(ref response, mission.Id, sumScore, isReceived);
}
else if (mission.MissionType == EventAZXAppleGameMissionMissionType.UseSkillCount)
{
int progress = 0;
if (azxInfo.SkillCount.TryGetValue(mission.MissionConditionId, out var skillCount)) progress = skillCount;
AddAchievementMission(ref response, mission.Id, progress, isReceived);
}
else if (mission.MissionType == EventAZXAppleGameMissionMissionType.PlayCharacterCount)
{
int progress = 0;
if (azxInfo.CharacterCount.TryGetValue(mission.MissionConditionId, out var characterCount)) progress = characterCount;
AddAchievementMission(ref response, mission.Id, progress, isReceived);
}
else
{
AddAchievementMission(ref response, mission.Id, 0, isReceived);
}
}
}
catch (Exception ex)
{
log.Error($"Get AchievementMissionDataList Error: {ex.Message}");
}
try
{
// ConditionalBoardDataList
azxInfo.ConditionalBoardDataList ??= [];
var boards = GameData.Instance.EventAZXAppleGameBoardTable.Values.ToList();
foreach (var board in boards)
{
var item = azxInfo.ConditionalBoardDataList.Find(x => x.BoardId == board.Id);
bool isUnlocked = item is not null && item.IsUnlocked;
if (board.BoardOpenScore == 0) continue;
response.ConditionalBoardDataList.Add(new NetMiniGameAzxConditionalBoardData()
{
BoardId = board.Id,
Progress = sumScore,
IsUnlocked = isUnlocked
});
}
// SelectedBoardId
int selectedBoardId = azxInfo.SelectedBoardId > 0 ? azxInfo.SelectedBoardId : boards.Min(x => x.Id);
response.SelectedBoardId = selectedBoardId;
}
catch (Exception ex)
{
log.Error($"Get ConditionalBoardDataList Error: {ex.Message}");
}
try
{
// ConditionalCharacterDataList
azxInfo.ConditionalCharacterDataList ??= [];
var characters = GameData.Instance.EventAZXAppleGameCharacterTable.Values.ToList();
foreach (var character in characters)
{
var item = azxInfo.ConditionalCharacterDataList.Find(x => x.CharacterId == character.Id);
bool isUnlocked = item is not null && item.IsUnlocked;
if (character.CharacterOpenScore == 0) continue;
response.ConditionalCharacterDataList.Add(new NetMiniGameAzxConditionalCharacterData()
{
CharacterId = character.Id,
Progress = sumScore,
IsUnlocked = isUnlocked
});
}
// SelectedCharacterId
int selectedCharacterId = azxInfo.SelectedCharacterId > 0 ? azxInfo.SelectedCharacterId : characters.Min(x => x.Id);
response.SelectedCharacterId = selectedCharacterId;
}
catch (Exception ex)
{
log.Error($"Get ConditionalCharacterDataList Error: {ex.Message}");
}
try
{
// ConditionalSkillDataList
azxInfo.ConditionalSkillDataList ??= [];
foreach (var skill in GameData.Instance.EventAZXAppleGameSkillTable.Values)
{
var item = azxInfo.ConditionalSkillDataList.Find(x => x.SkillId == skill.Id);
bool isUnlocked = item is not null && item.IsUnlocked;
if (skill.SkillOpenUseValue == 0) continue;
response.ConditionalSkillDataList.Add(new NetMiniGameAzxConditionalSkillData()
{
SkillId = skill.Id,
IsUnlocked = isUnlocked
});
}
}
catch (Exception ex)
{
log.Error($"Get ConditionalSkillDataList Error: {ex.Message}");
}
try
{
azxInfo.CutSceneDataList ??= [];
foreach (var cutScene in GameData.Instance.EventAZXAppleGameCutSceneTable.Values)
{
var item = azxInfo.CutSceneDataList.Find(x => x.CutSceneId == cutScene.Id);
if (item is not null && item.CutSceneId > 0)
{
response.CutSceneList.Add(new NetMiniGameAzxCutSceneData { CutSceneId = cutScene.Id, IsNew = item.IsNew });
}
}
}
catch (Exception ex)
{
Logging.WriteLine($"Get CutSceneList Error: {ex.Message}", LogType.Error);
}
try
{
// NetMiniGameAzxDailyMissionData DailyMissionData
azxInfo.ScoreDatas ??= [];
int dateDay = GetDateDay();
var scoreData = azxInfo.ScoreDatas.Find(x => x.DateDay == dateDay && x.AzxId == azxId);
if (scoreData is not null)
{
response.DailyMissionData = new NetMiniGameAzxDailyMissionData()
{
DailyAccumulatedScore = scoreData.AccumulatedScore,
IsDailyRewarded = scoreData.IsDailyRewarded,
};
}
}
catch (Exception ex)
{
Logging.WriteLine($"Get DailyMissionData Error: {ex.Message}", LogType.Error);
}
response.IsTutorialConfirmed = azxInfo.IsTutorialConfirmed;
}
public static void SetTutorialConfirmed(User user, int azxId)
{
log.Debug($"Setting tutorial confirmed for AZX {azxId}");
try
{
var azxInfo = GetAzxInfo(user, azxId);
azxInfo.IsTutorialConfirmed = true;
user.MiniGameAzxInfo[azxId] = azxInfo;
log.Debug($"Tutorial data after: {azxInfo.IsTutorialConfirmed}");
}
catch (Exception ex)
{
Logging.WriteLine($"Setting AZX tutorial confirmed failed: {ex.Message}", LogType.Error);
}
}
public static void SetBoardUnlocked(User user, int azxId, int boardId)
{
log.Debug($"Setting board {boardId} unlocked for AZX {azxId}");
try
{
var azxInfo = GetAzxInfo(user, azxId);
azxInfo.ConditionalBoardDataList ??= [];
log.Debug($"Board data before: {JsonConvert.SerializeObject(azxInfo.ConditionalBoardDataList)}");
var itemIndex = azxInfo.ConditionalBoardDataList.FindIndex(x => x.BoardId == boardId);
if (itemIndex >= 0)
{
azxInfo.ConditionalBoardDataList[itemIndex].IsUnlocked = true;
}
else
{
azxInfo.ConditionalBoardDataList.Add(new ConditionalBoardData() { BoardId = boardId, IsUnlocked = true });
}
user.MiniGameAzxInfo[azxId] = azxInfo;
log.Debug($"Board data after: {JsonConvert.SerializeObject(azxInfo.ConditionalBoardDataList)}");
}
catch (Exception ex)
{
Logging.WriteLine($"Setting AZX board unlocked failed: {ex.Message}", LogType.Error);
}
}
public static void SetSkillUnlocked(User user, int azxId, int skillId)
{
log.Debug($"Setting skill {skillId} unlocked for AZX {azxId}");
try
{
var azxInfo = GetAzxInfo(user, azxId);
azxInfo.ConditionalSkillDataList ??= [];
log.Debug($"Skill data before: {JsonConvert.SerializeObject(azxInfo.ConditionalSkillDataList)}");
var itemIndex = azxInfo.ConditionalSkillDataList.FindIndex(x => x.SkillId == skillId);
if (itemIndex >= 0)
{
azxInfo.ConditionalSkillDataList[itemIndex].IsUnlocked = true;
}
else
{
azxInfo.ConditionalSkillDataList.Add(new ConditionalSkillData() { SkillId = skillId, IsUnlocked = true });
}
user.MiniGameAzxInfo[azxId] = azxInfo;
log.Debug($"Skill data after: {JsonConvert.SerializeObject(azxInfo.ConditionalSkillDataList)}");
}
catch (Exception ex)
{
Logging.WriteLine($"Setting AZX skill unlocked failed: {ex.Message}", LogType.Error);
}
}
public static void SetCharacterUnlocked(User user, int azxId, int characterId)
{
log.Debug($"Setting character {characterId} unlocked for AZX {azxId}");
try
{
var azxInfo = GetAzxInfo(user, azxId);
azxInfo.ConditionalCharacterDataList ??= [];
log.Debug($"Character data before: {JsonConvert.SerializeObject(azxInfo.ConditionalCharacterDataList)}");
var itemIndex = azxInfo.ConditionalCharacterDataList.FindIndex(x => x.CharacterId == characterId);
if (itemIndex >= 0)
{
azxInfo.ConditionalCharacterDataList[itemIndex].IsUnlocked = true;
}
else
{
azxInfo.ConditionalCharacterDataList.Add(new ConditionalCharacterData() { CharacterId = characterId, IsUnlocked = true });
}
user.MiniGameAzxInfo[azxId] = azxInfo;
log.Debug($"Character data after: {JsonConvert.SerializeObject(azxInfo.ConditionalCharacterDataList)}");
}
catch (Exception ex)
{
Logging.WriteLine($"Setting AZX character unlocked failed: {ex.Message}", LogType.Error);
}
}
public static void SetCutSceneConfirmed(User user, int azxId, List<int> cutSceneIdList)
{
log.Debug($"Setting cutscenes confirmed for AZX {azxId}: {string.Join(", ", cutSceneIdList)}");
try
{
var azxInfo = GetAzxInfo(user, azxId);
azxInfo.CutSceneDataList ??= [];
log.Debug($"Cutscene data before: {JsonConvert.SerializeObject(azxInfo.CutSceneDataList)}");
foreach (var item in cutSceneIdList)
{
var itemIndex = azxInfo.CutSceneDataList.FindIndex(x => x.CutSceneId == item);
if (itemIndex >= 0)
{
azxInfo.CutSceneDataList[itemIndex].IsNew = false;
}
else
{
azxInfo.CutSceneDataList.Add(new CutSceneData() { CutSceneId = item, IsNew = false });
}
}
user.MiniGameAzxInfo[azxId] = azxInfo;
log.Debug($"Cutscene data after: {JsonConvert.SerializeObject(azxInfo.CutSceneDataList)}");
}
catch (Exception ex)
{
Logging.WriteLine($"Setting AZX cutscene confirmed failed: {ex.Message}", LogType.Error);
}
}
private static int GetDateDay()
{
// +4 每天4点重新计算
DateTime dateTime = DateTime.UtcNow.AddHours(4);
return dateTime.Year * 10000 + dateTime.Month * 100 + dateTime.Day;
}
private static void AddAchievementMission(ref ResGetMiniGameAzxData response, int missionId, int progress, bool isReceived)
{
response.AchievementMissionDataList.Add(new NetMiniGameAzxAchievementMissionData()
{
MissionId = missionId,
Progress = progress,
IsReceived = isReceived
});
}
public static MiniGameAzxData GetAzxInfo(User user, int azxId)
{
if (!user.MiniGameAzxInfo.TryGetValue(azxId, out var azxInfo))
{
log.Debug($"Creating new AZX info for {azxId}");
user.MiniGameAzxInfo.TryAdd(azxId, new MiniGameAzxData() { });
azxInfo = new MiniGameAzxData() { };
}
log.Debug($"Getting AZX info for {azxId}, data: {JsonConvert.SerializeObject(azxInfo)}");
return azxInfo;
}
}
}

View File

@@ -0,0 +1,32 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/enter")]
public class EnterAzx : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqEnterMiniGameAzx Fields
// int AzxId
// int PlayBoardId
// int PlayCharacterId
ReqEnterMiniGameAzx req = await ReadData<ReqEnterMiniGameAzx>();
User user = GetUser();
// ResEnterMiniGameAzx Fields
// int PreviousSRankCount
ResEnterMiniGameAzx response = new()
{
PreviousSRankCount = 0
};
if (req.AzxId > 0 && req.PlayBoardId > 0 && req.PlayCharacterId > 0)
AzxHelper.EnterAzx(user, ref response, req.AzxId, req.PlayBoardId, req.PlayCharacterId);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,22 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/finish")]
public class FinishAzx : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "azxId": 1, "scoreAndTime": { "score": 50000, "timeToScore": "110.122697s" },
// "playBoardId": 101, "playCharacterId": 101, "skillUseCountList": [ { "skillId": 102 } ], "cutSceneId": 10101 }
ReqFinishMiniGameAzx req = await ReadData<ReqFinishMiniGameAzx>();
User user = GetUser();
ResFinishMiniGameAzx response = new();
AzxHelper.FinishAzx(user, req, ref response);
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,43 @@
using EpinelPS.Data;
using EpinelPS.Utils;
using log4net;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/get/data")]
public class GetAzxData : LobbyMsgHandler
{
private static readonly ILog log = LogManager.GetLogger(typeof(LobbyMsgHandler));
protected override async Task HandleAsync()
{
// int AzxId
ReqGetMiniGameAzxData req = await ReadData<ReqGetMiniGameAzxData>();
User user = GetUser();
// ResGetMiniGameAzxData Fields
// NetMiniGameAzxDailyMissionData DailyMissionData
// RepeatedField<NetMiniGameAzxAchievementMissionData> AchievementMissionDataList
// RepeatedField<NetMiniGameAzxCutSceneData> CutSceneList
// int SelectedBoardId
// int SelectedCharacterId
// RepeatedField<NetMiniGameAzxConditionalBoardData> ConditionalBoardDataList
// RepeatedField<NetMiniGameAzxConditionalCharacterData> ConditionalCharacterDataList
// RepeatedField<NetMiniGameAzxConditionalSkillData> ConditionalSkillDataList
// bool IsTutorialConfirmed
ResGetMiniGameAzxData response = new()
{
DailyMissionData = new NetMiniGameAzxDailyMissionData()
{
DailyAccumulatedScore = 0,
IsDailyRewarded = false,
},
};
// TODO: Add implementation for AchievementMissionDataList, CutSceneList, etc.
AzxHelper.GetAzxData(user, req.AzxId, ref response);
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,23 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/get/ranking")]
public class GetAzxRanking : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// int AzxId
ReqGetMiniGameAzxRanking req = await ReadData<ReqGetMiniGameAzxRanking>();
User user = GetUser();
ResGetMiniGameAzxRanking response = new();
if (req.AzxId > 0)
AzxHelper.GetRanking(user, req.AzxId, ref response);
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,28 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/get/reddot/data")]
public class GetAzxRedDotData : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqGetMiniGameAzxRedDotData Fields
// int AzxId
ReqGetMiniGameAzxRedDotData req = await ReadData<ReqGetMiniGameAzxRedDotData>();
User user = GetUser();
// ResGetMiniGameAzxRedDotData Fields
// bool IsDailyMissionAvailable
// bool AchievementMissionRewardExists
ResGetMiniGameAzxRedDotData response = new()
{
IsDailyMissionAvailable = true,
AchievementMissionRewardExists = true
};
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,26 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/set/board/unlocked")]
public class SetAzxBoardUnlocked : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqSetMiniGameAzxBoardUnlocked Fields
// int AzxId
// int BoardId
ReqSetMiniGameAzxBoardUnlocked req = await ReadData<ReqSetMiniGameAzxBoardUnlocked>();
User user = GetUser();
ResSetMiniGameAzxBoardUnlocked response = new();
if (req.BoardId > 0 && req.AzxId > 0)
AzxHelper.SetBoardUnlocked(user, req.AzxId, req.BoardId);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,26 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/set/character/unlocked")]
public class SetAzxCharacterUnlocked : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqSetMiniGameAzxCharacterUnlocked Fields
// int AzxId
// int CharacterId
ReqSetMiniGameAzxCharacterUnlocked req = await ReadData<ReqSetMiniGameAzxCharacterUnlocked>();
User user = GetUser();
ResSetMiniGameAzxCharacterUnlocked response = new();
if (req.CharacterId > 0 && req.AzxId > 0)
AzxHelper.SetCharacterUnlocked(user, req.AzxId, req.CharacterId);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,26 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/set/cutscene/confirmed")]
public class SetAzxCutSceneConfirmed : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqEnterMiniGameAzx Fields
// int AzxId
// RepeatedField<int> ConfirmedCutSceneIdList
ReqSetMiniGameAzxCutSceneConfirmed req = await ReadData<ReqSetMiniGameAzxCutSceneConfirmed>();
User user = GetUser();
ResSetMiniGameAzxCutSceneConfirmed response = new();
if (req.AzxId > 0 && req.ConfirmedCutSceneIdList.Count > 0)
AzxHelper.SetCutSceneConfirmed(user, req.AzxId, [.. req.ConfirmedCutSceneIdList]);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,26 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/set/skill/unlocked")]
public class SetAzxSkillUnlocked : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqSetMiniGameAzxSkillUnlocked Fields
// int AzxId
// int SkillId
ReqSetMiniGameAzxSkillUnlocked req = await ReadData<ReqSetMiniGameAzxSkillUnlocked>();
User user = GetUser();
ResSetMiniGameAzxSkillUnlocked response = new();
if (req.SkillId > 0 && req.AzxId > 0)
AzxHelper.SetSkillUnlocked(user, req.AzxId, req.SkillId);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,22 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Minigame.AZX
{
[PacketPath("/event/minigame/azx/set/tutorial/confirmed")]
public class SetAzxTutorialConfirmed : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
await ReadData<ReqSetMiniGameAzxTutorialConfirmed>();
User user = GetUser();
ResSetMiniGameAzxTutorialConfirmed response = new();
AzxHelper.SetTutorialConfirmed(user, 1);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,188 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
using log4net;
using Newtonsoft.Json;
using static ResGetEventMissionClearList.Types;
namespace EpinelPS.LobbyServer.Event.Mission
{
public static class EventMissionHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(EventMissionHelper));
public static RepeatedField<NetEventMissionClearData> GetCleared(User user, int eventId)
{
var clearData = new RepeatedField<NetEventMissionClearData>();
if (!user.EventMissionInfo.TryGetValue(eventId, out var userEvent)) return clearData;
log.Debug($"GetClear UserEvent: {JsonConvert.SerializeObject(userEvent)}");
int dateDay = user.GetDateDay();
// Check if it's a new day, reset daily missions
if (userEvent.LastDay != dateDay)
{
ResetUserDailyMission(user, eventId, dateDay);
}
foreach (var id in userEvent.DailyMissionIdList)
{
clearData.Add(new NetEventMissionClearData()
{
EventId = eventId,
EventMissionId = id,
CreatedAt = userEvent.LastDate
});
}
foreach (var id in userEvent.MissionIdList)
{
clearData.Add(new NetEventMissionClearData()
{
EventId = eventId,
EventMissionId = id,
CreatedAt = userEvent.LastDate
});
}
return clearData;
}
public static RepeatedField<NestEventMissionClear> GetClearedList(User user, RepeatedField<int> eventIds)
{
var clearDatas = new RepeatedField<NestEventMissionClear>();
if (eventIds.Count == 0) return clearDatas;
foreach (var eventId in eventIds)
{
var clearData = new NestEventMissionClear
{
EventId = eventId
};
clearData.EventMissionClearList.AddRange(GetCleared(user, eventId));
}
return clearDatas;
}
/// <summary>
/// Obtain reward for event mission
/// </summary>
/// <param name="user"></param>
/// <param name="reward"></param>
/// <param name="eventId"></param>
/// <param name="missionIds"></param>
/// <param name="timeStamp"></param>
public static void ObtainReward(User user, ref NetRewardData reward, int eventId, RepeatedField<int> missionIds, Timestamp timeStamp)
{
EventMissionData userEvent = GetUserEventMissionData(user, eventId);
log.Debug($"ObtainReward UserEvent Before: {JsonConvert.SerializeObject(userEvent)}");
int dateDay = user.GetDateDay();
// Check if it's a new day, reset daily missions
if (userEvent.LastDay != dateDay)
{
log.Debug($"ObtainReward New Day: {dateDay}");
ResetUserDailyMission(user, eventId, dateDay);
}
var userMissionIds = userEvent.MissionIdList ?? [];
var userDailyMissionIds = userEvent.DailyMissionIdList ?? [];
var eventMissionRecords = GameData.Instance.EventMissionListTable.Values.Where(em =>
missionIds.Contains(em.Id)
&& !userMissionIds.Contains(em.Id)
&& !userDailyMissionIds.Contains(em.Id)).ToList();
if (eventMissionRecords.Count == 0) return;
log.Debug($"ObtainReward Event Mission Records: {JsonConvert.SerializeObject(eventMissionRecords)}");
List<Reward_Data> rewards = [];
foreach (var mission in eventMissionRecords)
{
if (mission.RewardId == 0)
{
if (mission.RewardPointValue > 0)
{
user.AddTrigger(Trigger.PointRewardEvent, mission.RewardPointValue, mission.Group);
}
continue;
}
user.AddTrigger(Trigger.MissionClearEvent, 1, mission.Group);
var rewardRecord = GameData.Instance.GetRewardTableEntry(mission.RewardId);
if (rewardRecord is null || rewardRecord.Rewards.Count == 0) continue;
foreach (var item in rewardRecord.Rewards)
{
var itemIndex = rewards.FindIndex(x => x.RewardId == item.RewardId);
if (itemIndex >= 0)
rewards[itemIndex].RewardValue += item.RewardValue;
else
rewards.Add(item);
}
}
// if (rewards.Count == 0) return;
log.Debug($"ObtainReward Rewards: {JsonConvert.SerializeObject(rewards)}");
// Add rewards to user
foreach (var r in rewards)
{
RewardUtils.AddSingleObject(user, ref reward, r.RewardId, r.RewardType, r.RewardValue);
}
// Add mission ids to user
var groupIds = eventMissionRecords.Select(x => x.Group).Distinct();
var categoryRecords = GameData.Instance.EventMissionCategoryTable.Values.Where(ec => groupIds.Contains(ec.MissionListGroup)).ToList();
if (categoryRecords.Count == 0) return;
foreach (var mission in eventMissionRecords)
{
var categoryRecord = categoryRecords.FirstOrDefault(ec => ec.MissionListGroup == mission.Group);
if (categoryRecord is null) continue;
if (categoryRecord.InitType == EventMissionInitType.Daily)
{
userEvent.DailyMissionIdList.Add(mission.Id);
}
else
{
userEvent.MissionIdList.Add(mission.Id);
}
}
foreach (var item in reward.Item)
{
user.AddTrigger(Trigger.ObtainEventCurrencyMaterial, item.Count, item.Tid);
}
userEvent.LastDate = timeStamp.ToDateTime().Ticks;
user.EventMissionInfo[eventId] = userEvent;
log.Debug($"ObtainReward UserEvent After: {JsonConvert.SerializeObject(userEvent)}");
JsonDb.Save();
}
private static void ResetUserDailyMission(User user, int eventId, int dateDay)
{
if (!user.EventMissionInfo.TryGetValue(eventId, out var userEvent)) return;
if (userEvent.LastDay == dateDay) return;
user.EventMissionInfo[eventId].DailyMissionIdList = [];
user.EventMissionInfo[eventId].LastDay = dateDay;
JsonDb.Save();
}
/// <summary>
/// Get user event mission data, if not exists, create a new one
/// </summary>
/// <param name="user">User</param>
/// <param name="eventId">EventId</param>
/// <returns>EventMissionData</returns>
private static EventMissionData GetUserEventMissionData(User user, int eventId)
{
// Get user event mission data, if not exists, create a new one
if (!user.EventMissionInfo.TryGetValue(eventId, out var userEvent))
{
userEvent = new EventMissionData();
user.EventMissionInfo.Add(eventId, userEvent);
}
return userEvent;
}
}
}

View File

@@ -1,19 +0,0 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Mission
{
[PacketPath("/event/mission/getclearlist")]
public class GetClearList : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetEventMissionClearList req = await ReadData<ReqGetEventMissionClearList>();
ResGetEventMissionClearList response = new(); //field ResGetEventMissionClearMap data type NestEventMissionClear field NestEventMissionClear data type NetEventMissionClearData fields EventId EventMissionId CreatedAt
// TOOD
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,29 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Mission
{
[PacketPath("/event/mission/getclear")]
public class GetMissionClear : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqGetEventMissionClear>(); //EventId
User user = GetUser();
ResGetEventMissionClear response = new();
try
{
response.EventMissionClearList.AddRange(EventMissionHelper.GetCleared(user, req.EventId));
}
catch (Exception ex)
{
Logging.Warn($"GetMissionClear failed: {ex.Message}");
}
// TODO
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,27 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Mission
{
[PacketPath("/event/mission/getclearlist")]
public class GetMissionClearList : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "eventIdList": [ 60090, 60092, 20001, 20002 ] }
ReqGetEventMissionClearList req = await ReadData<ReqGetEventMissionClearList>();
User user = GetUser();
ResGetEventMissionClearList response = new();
try
{
response.ResGetEventMissionClearMap.AddRange(EventMissionHelper.GetClearedList(user, req.EventIdList));
}
catch (Exception ex)
{
Logging.Warn($"GetMissionClearList failed: {ex.Message}");
}
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,40 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Mission
{
[PacketPath("/event/mission/reward")]
public class ObtainMissionReward : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "eventId": 20001, "dailyEventId": [ 200010105 ] }
// ReqObtainEventMissionReward Fields
// int EventId
// RepeatedField<int> EventMissionIdList
// Google.Protobuf.WellKnownTypes.Timestamp RequestTimeStamp
var req = await ReadData<ReqObtainEventMissionReward>();
User user = GetUser();
// ResObtainEventMissionReward Fields
// NetRewardData Reward
// ObtainEventMissionRewardResult Result
ResObtainEventMissionReward response = new()
{
Result = ObtainEventMissionRewardResult.Success
};
var reward = new NetRewardData();
try
{
EventMissionHelper.ObtainReward(user, ref reward, req.EventId, req.EventMissionIdList, req.RequestTimeStamp);
}
catch (Exception ex)
{
Logging.Warn($"ObtainMissionReward failed: {ex.Message}");
}
response.Reward = reward;
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,26 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Shop
{
[PacketPath("/event/shopbuyproduct")]
public class BuyProduct : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// int EventId
var req = await ReadData<ReqEventShopBuyProduct>();
ResEventShopBuyProduct response = new();
User user = GetUser();
try
{
EventShopHelper.BuyShopProduct(user, ref response, req);
}
catch (Exception ex)
{
Logging.WriteLine($"Error buying shop product: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,495 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Event.Shop
{
public static class EventShopHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(EventShopHelper));
public static void BuyShopProduct(User user, ref ResEventShopBuyProduct response, ReqEventShopBuyProduct req)
{
ResEventShopMultipleBuyProduct MultipleResponse = new();
List<NetBuyProductRequestData> buyProducts = [new() { ShopProductTid = req.ShopProductTid, Quantity = req.Quantity, Order = req.Order }];
bool isSuccess = ExecuteBuyProduct(user, ref MultipleResponse, buyProducts);
if (!isSuccess) return;
AddEventShopBuyCount(user, ref MultipleResponse, req.EventId, req.ShopProductTid, req.Quantity, req.Order);
JsonDb.Save();
// Update currency data
if (MultipleResponse.Currencies.Count > 0)
response.Currencies.AddRange(MultipleResponse.Currencies);
// Update item data
if (MultipleResponse.Items.Count > 0)
response.Item = MultipleResponse.Items[0];
// Update product data
response.Product = new();
if (MultipleResponse.Product.UserItems.Count > 0) response.Product.UserItems.AddRange(MultipleResponse.Product.UserItems);
if (MultipleResponse.Product.Item.Count > 0) response.Product.Item.AddRange(MultipleResponse.Product.Item);
if (MultipleResponse.Product.Currency.Count > 0) response.Product.Currency.AddRange(MultipleResponse.Product.Currency);
if (MultipleResponse.Product.BuyCounts.Count > 0) response.Product.BuyCount = MultipleResponse.Product.BuyCounts[0].BuyCount;
if (MultipleResponse.Product.Character.Count > 0) response.Product.Character.AddRange(MultipleResponse.Product.Character);
if (MultipleResponse.Product.UserCharacters.Count > 0) response.Product.UserCharacters.AddRange(MultipleResponse.Product.UserCharacters);
if (MultipleResponse.Product.AutoCharge.Count > 0) response.Product.AutoCharge.AddRange(MultipleResponse.Product.AutoCharge);
// user.AddTrigger(Trigger.MainShopBuy, req.Quantity);
// Save changes to the database
JsonDb.Save();
}
public static void BuyShopMultipleProduct(User user, ref ResEventShopMultipleBuyProduct response, ReqEventShopMultipleBuyProduct req)
{
bool isSuccess = ExecuteBuyProduct(user, ref response, [.. req.Products]);
if (!isSuccess) return;
foreach (var item in req.Products)
{
AddEventShopBuyCount(user, ref response, req.EventId, item.ShopProductTid, item.Quantity, item.Order);
}
JsonDb.Save();
}
private static void AddEventShopBuyCount(User user, ref ResEventShopMultipleBuyProduct response, int eventId, int productTid, int quantity, int order)
{
if (!user.EventShopBuyCountInfo.TryGetValue(eventId, out var buyCountInfo))
{
buyCountInfo = new() { EventId = eventId, datas = [] };
user.EventShopBuyCountInfo.TryAdd(eventId, buyCountInfo);
}
var index = buyCountInfo.datas.FindIndex(x => x.ProductTid == productTid);
if (index >= 0)
{
buyCountInfo.datas[index].BuyCount += quantity;
response.Product.BuyCounts.Add(new NetBuyCountData { Order = order, BuyCount = buyCountInfo.datas[index].BuyCount });
}
else
{
buyCountInfo.datas.Add(new() { ProductTid = productTid, BuyCount = quantity });
response.Product.BuyCounts.Add(new NetBuyCountData { Order = order, BuyCount = quantity });
}
user.EventShopBuyCountInfo[eventId] = buyCountInfo;
}
private static bool ExecuteBuyProduct(User user, ref ResEventShopMultipleBuyProduct response, List<NetBuyProductRequestData> buyProducts)
{
if (buyProducts == null || buyProducts.Count == 0) return false;
response.Product = new();
var productTids = buyProducts.Select(p => p.ShopProductTid).ToList();
var shopProducts = GameData.Instance.ContentsShopProductTable.Values.Where(x => productTids.Contains(x.Id)).ToList();
// Check user currency and item balance
if (CheckUserCurrencyAndItemBalance(user, shopProducts, buyProducts, out Dictionary<int, int> totalCurrencyPrice, out Dictionary<int, int> totalItemPrice))
{
// Deduct user currency and item
DeductUserCurrencyAndItems(user, totalCurrencyPrice, totalItemPrice, ref response);
}
else
{
return false;
}
// Process each shopProduct
foreach (var shopProduct in shopProducts)
{
var buyProduct = buyProducts.FirstOrDefault(bp => bp.ShopProductTid == shopProduct.Id);
int quantity = buyProduct.Quantity;
int order = buyProduct.Order;
if (shopProduct.GoodsType == RewardType.Item || shopProduct.GoodsType.ToString().StartsWith("Equipment"))
{
AddItemById(user, ref response, itemId: shopProduct.GoodsId, RewardType.Item, shopProduct.GoodsValue, quantity, order);
}
else if (shopProduct.GoodsType == RewardType.Currency)
{
long val = shopProduct.GoodsValue * quantity;
user.AddCurrency((CurrencyType)shopProduct.GoodsId, val);
// buyCounts.Add(new() { Order = order, BuyCount = quantity });
response.Product.Currency.Add(new NetCurrencyData() { Type = shopProduct.GoodsId, Value = val, FinalValue = user.GetCurrencyVal((CurrencyType)shopProduct.GoodsId) });
}
else if (shopProduct.GoodsType == RewardType.Character)
{
AddCharacterByCharacterTid(user, ref response, shopProduct.GoodsId, shopProduct.GoodsValue, quantity, order);
}
else if (shopProduct.GoodsType == RewardType.UserTitle)
{
response.Product.UserTitleList.Add(shopProduct.GoodsId);
}
else if (shopProduct.GoodsType == RewardType.LiveWallpaper)
{
response.Product.LiveWallPapers.Add(shopProduct.GoodsId);
}
else
{
Logging.WriteLine($"Unsupported GoodsType: {shopProduct.GoodsType}");
}
}
JsonDb.Save();
return true;
}
private static void DeductUserCurrencyAndItems(User user, Dictionary<int, int> totalCurrencyPrice, Dictionary<int, int> totalItemPrice,
ref ResEventShopMultipleBuyProduct response)
{
foreach (int key in totalCurrencyPrice.Keys)
{
CurrencyType currencyType = (CurrencyType)key;
user.SubtractCurrency(currencyType, totalCurrencyPrice[key]);
response.Currencies.Add(new NetUserCurrencyData() { Type = key, Value = user.GetCurrencyVal(currencyType) });
}
foreach (int tid in totalItemPrice.Keys)
{
var item = user.Items.FirstOrDefault(i => i.ItemType == tid);
user.RemoveItemBySerialNumber(item.Isn, totalItemPrice[tid]);
response.Items.Add(new NetUserItemData() { Tid = tid, Count = user.Items.FirstOrDefault(i => i.ItemType == tid).Count, Isn = item.Isn });
}
}
private static bool CheckUserCurrencyAndItemBalance(User user, List<ContentsShopProductRecord> shopProducts, List<NetBuyProductRequestData> buyProducts,
out Dictionary<int, int> totalCurrencyPrices, out Dictionary<int, int> totalItemPrices)
{
totalCurrencyPrices = shopProducts
.Where(sp => sp.PriceType == PriceType.Currency)
.GroupBy(sp => sp.PriceId)
.ToDictionary(
g => g.Key,
g => g.Sum(sp => sp.PriceValue * buyProducts.FirstOrDefault(bp => bp.ShopProductTid == sp.Id).Quantity)
);
totalItemPrices = shopProducts
.Where(sp => sp.PriceType == PriceType.Item)
.GroupBy(sp => sp.PriceId)
.ToDictionary(
g => g.Key,
g => g.Sum(sp => sp.PriceValue * buyProducts.FirstOrDefault(bp => bp.ShopProductTid == sp.Id).Quantity)
);
Logging.WriteLine($"totalCurrencyPrice: {JsonConvert.SerializeObject(totalCurrencyPrices)}", LogType.Debug);
foreach (int currencyType in totalCurrencyPrices.Keys)
{
var userCurrency = user.Currency.FirstOrDefault(x => x.Key == (CurrencyType)currencyType).Value;
if (userCurrency < totalCurrencyPrices[currencyType])
{
Logging.WriteLine($"Insufficient funds: Have {userCurrency}, need {totalCurrencyPrices[currencyType]}");
return false;
}
}
Logging.WriteLine($"totalItemPrice: {JsonConvert.SerializeObject(totalItemPrices)}", LogType.Debug);
foreach (int tid in totalItemPrices.Keys)
{
var item = user.Items.FirstOrDefault(i => i.ItemType == tid);
if (item == null || item.Count < totalItemPrices[tid])
{
Logging.WriteLine($"Insufficient item funds: Have {item?.Count ?? 0}, need {totalItemPrices[tid]}");
return false;
}
}
return true;
}
public static int GetEventShopId(int eventId)
{
if (eventId <= 0) return 0;
if (GameData.Instance.eventManagers.TryGetValue(eventId, out var eventRecord))
{
if (eventRecord.EventShortcutId is not null && eventRecord.EventShortcutId != "")
{
return Convert.ToInt32(eventRecord.EventShortcutId);
}
}
log.Warn($"EventManager not found for EventId: {eventId}");
return 0;
}
/// <summary>
/// Initialize Shop Data
/// </summary>
/// <param name="shopId">Shop ID</param>
/// <returns>Shop Data</returns>
public static NetEventShopProductData InitShopData(User user, int eventId)
{
NetEventShopProductData shop = new();
var shopId = GetEventShopId(eventId);
if (shopId <= 0) return shop;
try
{
if (!GameData.Instance.ContentsShopTable.TryGetValue(shopId, out var tableShop))
{
Logging.WriteLine($"Invalid shopId: {shopId}", LogType.Warning);
return shop;
}
var userBuyCounts = new List<EventShopProductData>();
if (user.EventShopBuyCountInfo.TryGetValue(eventId, out var userBuyCountInfo)) userBuyCounts = userBuyCountInfo.datas;
shop.ShopTid = tableShop.Id;
shop.ShopCategory = (int)tableShop.ShopCategory;
GameData.Instance.ContentsShopProductTable.Values
.Where(csp => csp.BundleId == tableShop.BundleId).ToList().ForEach(csp =>
{
var buyCount = userBuyCounts.FirstOrDefault(x => x.ProductTid == csp.Id)?.BuyCount ?? 0;
shop.List.Add(new NetShopProductInfoData
{
Order = csp.ProductOrder,
ProductId = csp.Id,
BuyLimitCount = csp.BuyLimitCount,
BuyCount = buyCount,
// Discount = csp.DiscountProbId,
});
});
return shop;
}
catch (Exception ex)
{
Logging.WriteLine($"Error in InitShopData: {ex}");
return shop;
}
}
public static List<string> GetShopIds()
{
List<string> shopIds = [];
var ContentsShopTable = GameData.Instance.ContentsShopTable;
var eventShops = ContentsShopTable.Values.Where(s => s.ShopCategory == ShopCategoryType.ShopStoryEvent || s.ShopType == ShopType.EventShop).ToList();
foreach (var shop in eventShops)
{
shopIds.Add(shop.Id.ToString());
}
log.Debug($"Final list of shop IDs: {JsonConvert.SerializeObject(shopIds)}");
return shopIds;
}
public static bool UpdateCurrency(User user, int priceId, int priceValue, int quantity, ref ResEventShopBuyProduct response)
{
long totalPrice = priceValue * quantity;
if (!user.Currency.TryGetValue((CurrencyType)priceId, out var currentAmount))
{
Logging.WriteLine($"Insufficient funds: Have {currentAmount}, need {totalPrice}");
return false;
}
if (currentAmount < totalPrice)
{
Logging.WriteLine($"Insufficient funds: Have {currentAmount}, need {totalPrice}");
return false;
}
CurrencyType currencyType = (CurrencyType)priceId; // Assuming PriceId maps directly to CurrencyType
long newAmount = currentAmount - totalPrice; // calculate new amount
user.Currency[currencyType] = newAmount; // update user currency
response.Currencies.Add(new NetUserCurrencyData // Update response currency
{
Type = (int)currencyType,
Value = newAmount
});
return true;
}
public static bool UpdateItem(User user, int priceId, int priceValue, int quantity, ref ResEventShopBuyProduct response)
{
var item = user.Items.FirstOrDefault(i => i.ItemType == priceId);
if (item == null || item.Count < quantity)
{
Logging.WriteLine($"Insufficient item funds: Have {item?.Count ?? 0}, need {priceValue * quantity}");
return false; // Not enough items
}
else
{
item.Count -= priceValue * quantity; // Deduct the item cost
if (item.Count <= 0)
{
user.Items.Remove(item); // Remove item if count is zero or less
}
// Update response items
response.Item = new()
{
Tid = item.ItemType,
Count = item.Count,
Isn = item.Isn
};
}
return true;
}
public static void AddCharacterByCharacterTid(User user, ref ResEventShopMultipleBuyProduct response, int characterTid, int goodsValue, int quantity, int order)
{
// Get character data from GameData.Instance.CharacterTable
if (!GameData.Instance.CharacterTable.TryGetValue(characterTid, out var characterRecord))
{
return; // Character data not found, return
}
// Check if character already exists in user.Characters
var userCharacter = user.GetCharacter(characterTid);
bool isAddNewCharacter = userCharacter == null;
if (isAddNewCharacter)
{
// Add new character to user.Characters
userCharacter = new CharacterModel()
{
Csn = user.GenerateUniqueCharacterId(),
Grade = 1,
Tid = characterRecord.Id,
};
user.Characters.Add(userCharacter);
response.Product.UserCharacters.Add(ToNetUserCharacter(userCharacter));
}
NetCharacterData netCharacter = new() { Csn = userCharacter.Csn, Tid = userCharacter.Tid };
// Calculate character material num
int characterMaterialNum = isAddNewCharacter ? goodsValue * quantity - 1 : goodsValue * quantity;
if (characterMaterialNum > 0)
{
var currentOriginalRare = characterRecord.OriginalRare;
// Get max core num
int maxCoreNum = currentOriginalRare == OriginalRareType.SSR ? 11 : currentOriginalRare == OriginalRareType.SR ? 3 : 1;
// Get current core num
int currentCoreNum = currentOriginalRare == OriginalRareType.SSR ? userCharacter.Grade : currentOriginalRare == OriginalRareType.SR ? userCharacter.Grade % 100 : 1;
// If current core num is greater than max core num, set current core num to max core num
if (currentCoreNum > maxCoreNum) currentCoreNum = maxCoreNum;
int currentMaterialNum = user.Items.FirstOrDefault(x => x.ItemType == characterRecord.PieceId)?.Count ?? 0;
bool isAddMaterial = currentCoreNum < maxCoreNum;
int addMaterialNum = characterMaterialNum - currentMaterialNum;
int addCurrencyNum = 0;
bool isAddCurrency = currentCoreNum + addMaterialNum > maxCoreNum;
if (isAddCurrency)
{
int MaterialCurrencyNum = currentOriginalRare == OriginalRareType.SSR ? 6000 : currentOriginalRare == OriginalRareType.SR ? 200 : 150;
addCurrencyNum = (currentCoreNum + addMaterialNum - maxCoreNum) * MaterialCurrencyNum;
addMaterialNum = maxCoreNum - currentCoreNum;
}
Dictionary<CurrencyType, long> currency = [];
if (addCurrencyNum > 0)
{
netCharacter.CurrencyValue = addCurrencyNum;
user.AddCurrency(CurrencyType.DissolutionPoint, addCurrencyNum);
currency.Add(CurrencyType.DissolutionPoint, addCurrencyNum);
}
List<ItemData> items = [];
List<ItemData> userItems = [];
if (addMaterialNum > 0)
{
netCharacter.PieceCount = addMaterialNum;
AddItemById(user, ref response, characterRecord.PieceId, RewardType.Item, goodsValue, addMaterialNum, order);
}
response.Product.Character.Add(netCharacter);
}
}
public static void AddItemById(User user, ref ResEventShopMultipleBuyProduct response,
int itemId, RewardType itemType, int goodsValue, int quantity, int order)
{
var userItemIndex = user.Items.FindIndex(i => i.ItemType == itemId);
var isEquip = GameData.Instance.ItemEquipTable.TryGetValue(itemId, out var equip);
if (userItemIndex >= 0)
{
if (isEquip)
{
// the item is not stackable, we need to create new entries for each quantity
for (int i = 0; i < goodsValue * quantity; i++)
{
var (tid, pos, isn) = (itemId, GetItemPos(equip.ItemSubType), user.GenerateUniqueItemId());
ItemData newItem = new() { ItemType = tid, Count = 1, Position = pos, Isn = isn, Corp = GetEquipCorp(itemType) };
user.Items.Add(newItem);
response.Product.Item.Add(NetUtils.ItemDataToNet(newItem));
response.Product.UserItems.Add(NetUtils.UserItemDataToNet(newItem));
}
}
else
{
user.Items[userItemIndex].Count += goodsValue * quantity;
response.Product.UserItems.Add(NetUtils.UserItemDataToNet(user.Items[userItemIndex]));
var (tid, count, isn) = (itemId, goodsValue * quantity, user.Items[userItemIndex].Isn);
bool isAddAutoCharge = AddAutoChargeByTid(ref response, itemId: itemId, value: count, finalValue: user.Items[userItemIndex].Count);
if (!isAddAutoCharge)
{
response.Product.Item.Add(new NetItemData() { Tid = tid, Count = count, Isn = isn });
}
}
}
else
{
var (tid, count, isn) = (itemId, goodsValue * quantity, user.GenerateUniqueItemId());
ItemData itemData = new() { ItemType = tid, Count = count, Isn = isn };
user.Items.Add(itemData);
response.Product.UserItems.Add(NetUtils.UserItemDataToNet(itemData));
bool isAddAutoCharge = AddAutoChargeByTid(ref response, itemId: itemId, value: count, finalValue: count);
if (!isAddAutoCharge)
{
response.Product.Item.Add(NetUtils.ItemDataToNet(itemData));
}
}
}
public static bool AddAutoChargeByTid(ref ResEventShopMultipleBuyProduct response, int itemId, int value, int finalValue)
{
var autoCharge = GameData.Instance.AutoChargeTable.Values.FirstOrDefault(x => x.ItemId == itemId);
if (autoCharge is null) return false;
response.Product.AutoCharge.Add(new NetAutoChargeData() { AutoChargeId = autoCharge.Id, Value = value, FinalValue = finalValue });
return true;
}
public static int GetItemPos(ItemSubType subType)
{
return subType switch
{
ItemSubType.ModuleA => 0,
ItemSubType.ModuleB => 1,
ItemSubType.ModuleC => 2,
ItemSubType.ModuleD => 3,
_ => 0,
};
}
public static int GetEquipCorp(RewardType subType)
{
return subType switch
{
RewardType.EquipmentELYSION => 1,
RewardType.EquipmentMISSILIS => 2,
RewardType.EquipmentTETRA => 3,
RewardType.EquipmentPILGRIM => 4,
RewardType.EquipmentABNORMAL => 7,
_ => 0,
};
}
public static NetUserCharacterDefaultData ToNetUserCharacter(CharacterModel character)
{
return new()
{
CostumeId = character.CostumeId,
Csn = character.Csn,
Grade = character.Grade,
Lv = character.Level,
UltiSkillLv = character.UltimateLevel,
Skill1Lv = character.Skill1Lvl,
Skill2Lv = character.Skill2Lvl,
Tid = character.Tid,
};
}
}
}

View File

@@ -1,24 +0,0 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Shop
{
[PacketPath("/event/shopproductlist")]
public class ListProductList : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqShopProductList req = await ReadData<ReqShopProductList>();
User user = GetUser();
ResShopProductList response = new();
response.Shops.Add(new NetShopProductData()
{
});
// TODO implement properly
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,45 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Shop
{
[PacketPath("/event/shopmultiplebuyproduct")]
public class MultipleBuyProduct : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// ReqEventShopMultipleBuyProduct Fileds
// int EventId
// int ShopCategory
// RepeatedField<NetBuyProductRequestData> Products
// NetBuyProductRequestData Fileds
// int ShopProductTid
// int Order
// int Quantity
var req = await ReadData<ReqEventShopMultipleBuyProduct>();
// ResEventShopMultipleBuyProduct Fileds
// EventShopBuyProductResult Result
// NetShopBuyMultipleProductData Product
// RepeatedField<NetUserItemData> Items
// RepeatedField<NetUserCurrencyData> Currencies
ResEventShopMultipleBuyProduct response = new()
{
Result = EventShopBuyProductResult.Success
};
User user = GetUser();
try
{
EventShopHelper.BuyShopMultipleProduct(user, ref response, req);
}
catch (Exception ex)
{
Logging.WriteLine($"Error buying shop product: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,27 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Shop
{
[PacketPath("/event/shopproductlist")]
public class ProductList : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// int EventId
var req = await ReadData<ReqEventShopProductList>();
ResEventShopProductList response = new();
User user = GetUser();
try
{
response.Shops.Add(EventShopHelper.InitShopData(user, req.EventId));
}
catch (Exception ex)
{
Logging.WriteLine($"Get EventShopProductList Error: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,54 @@
using EpinelPS.Utils;
using EpinelPS.Database;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Event.StoryEvent
{
[PacketPath("/event/storydungeon/clearstage")]
public class ClearEventStage : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqClearEventStage req = await ReadData<ReqClearEventStage>();
User user = GetUser();
ResClearEventStage response = new();
int difficultId = 0;
NetRewardData reward = new();
NetRewardData bonusReward = new();
ClearEventStageHelper.ClearStage(user, req.StageId, ref reward, ref bonusReward, req.BattleResult, 1); // always clearCount = 1 for normal clear
if (user.EventInfo.TryGetValue(req.EventId, out EventData? eventData) && req.BattleResult == 1)
{
if (!eventData.ClearedStages.Contains(req.StageId))
{
eventData.ClearedStages.Add(req.StageId);
}
if (eventData.LastStage < req.StageId) eventData.LastStage = req.StageId;
eventData.Diff = difficultId;
}
else
{
user.EventInfo.Add(req.EventId, new EventData() { LastStage = req.StageId, ClearedStages = [req.StageId] });
}
user.AddTrigger(Trigger.EventStageClear, 1, req.StageId);
user.AddTrigger(Trigger.EventDungeonStageClear, 1, req.EventId);
if (bonusReward.Item.Count > 0)
{
bonusReward.Item.ToList().ForEach(item =>
{
user.AddTrigger(Trigger.ObtainEventCurrencyMaterial, item.Count, item.Tid);
});
}
response.RemainTicket = EventStoryHelper.SubtractTicket(user, req.EventId, 1);
response.Reward = reward;
response.BonusReward = bonusReward;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,116 @@
using EpinelPS.Data;
using EpinelPS.Utils;
using log4net;
namespace EpinelPS.LobbyServer.Event.StoryEvent
{
public static class ClearEventStageHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(ClearEventStageHelper));
/// <summary>
/// Clear event stage and get rewards
/// </summary>
/// <param name="user">The user clearing the stage</param>
/// <param name="stageId">The ID of the stage being cleared</param>
/// <param name="reward">The reward data for the stage</param>
/// <param name="bonusReward">The bonus reward data for the stage</param>
/// <param name="battleResult">The result of the battle</param>
/// <param name="clearCount">The number of times the stage has been cleared</param>
public static void ClearStage(User user, int stageId, ref NetRewardData reward, ref NetRewardData bonusReward, int battleResult = 0, int clearCount = 0)
{
if (battleResult != 1) return;
if (clearCount < 1) clearCount = 1;
GetReward(user, stageId, ref reward, clearCount);
GetBonusReward(user, stageId, ref bonusReward, clearCount);
}
/// <summary>
/// Get normal reward for clearing event stage
/// </summary>
/// <param name="user">The user clearing the stage</param>
/// <param name="stageId">The ID of the stage being cleared</param>
/// <param name="reward">The reward data for the stage</param>
/// <param name="battleResult">The result of the battle</param>
/// <param name="clearCount">The number of times the stage has been cleared</param>
public static void GetReward(User user, int stageId, ref NetRewardData reward, int clearCount)
{
int rewardId = GetRewardId(stageId);
if (rewardId == 0) return;
RecievedReward(user, ref reward, rewardId, clearCount);
}
/// <summary>
/// Get bonus reward for clearing event stage
/// </summary>
/// <param name="user">The user clearing the stage </param>
/// <param name="stageId">The ID of the stage being cleared</param>
/// <param name="bonusReward">The bonus reward data for the stage</param>
/// <param name="battleResult">The result of the battle</param>
/// <param name="clearCount">The number of times the stage has been cleared</param>
public static void GetBonusReward(User user, int stageId, ref NetRewardData bonusReward, int clearCount)
{
int rewardId = GetBonusRewardId(stageId);
if (rewardId == 0) return;
RecievedReward(user, ref bonusReward, rewardId, clearCount);
}
private static void RecievedReward(User user, ref NetRewardData reward,int rewardId, int clearCount)
{
RewardRecord? rewardData = GameData.Instance.GetRewardTableEntry(rewardId);
if (rewardData == null)
{
Logging.WriteLine($"unknown reward Id {rewardId}", LogType.Error);
return;
}
foreach (var item in rewardData.Rewards)
{
if (item == null) continue;
if (item.RewardType == RewardType.None) continue;
RewardUtils.AddSingleObject(user, ref reward, item.RewardId, item.RewardType, item.RewardValue * clearCount);
}
}
/// <summary>
/// Get reward Id from EventDungeonSpotBattleTable
/// </summary>
/// <param name="stageId">The ID of the stage being cleared</param>
/// <returns>The reward ID for the stage</returns>
private static int GetRewardId(int stageId)
{
if (GameData.Instance.EventDungeonSpotBattleTable.TryGetValue(stageId, out EventDungeonSpotBattleRecord? stageRecord))
{
return stageRecord.ClearRewardId;
}
return 0;
}
/// <summary>
/// Get bonus reward Id from EventDungeonTable via EventDungeonStageTable and EventDungeonDifficultTable
/// </summary>
/// <param name="stageId">The ID of the stage being cleared</param>
/// <returns>The bonus reward ID for the stage</returns>
private static int GetBonusRewardId(int stageId)
{
if (!GameData.Instance.EventDungeonStageTable.TryGetValue(stageId, out EventDungeonStageRecord? eventStage))
{
log.Error($"EventDungeonStageTable not found for StageId: {stageId}");
return 0;
}
EventDungeonDifficultRecord? difficult = GameData.Instance.EventDungeonDifficultTable.Values.FirstOrDefault(x => x.StageGroup == eventStage.Group);
if (difficult == null)
{
log.Error($"EventDungeonDifficultTable not found for Group: {eventStage.Group}");
return 0;
}
EventDungeonRecord? dungeon = GameData.Instance.EventDungeonTable.Values.FirstOrDefault(x => x.DifficultGroup == difficult.Group);
if (dungeon == null)
{
log.Error($"EventDungeonTable not found for DifficultGroup: {difficult.Group}");
return 0;
}
return dungeon.BonusRewardId;
}
}
}

View File

@@ -0,0 +1,139 @@
using EpinelPS.Data;
using EpinelPS.Utils;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Event.StoryEvent
{
public static class EventStoryHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(EventStoryHelper));
/// <summary>
/// Get user remain ticket by event id
/// </summary>
/// <param name="user"></param>
/// <param name="eventId"></param>
/// <returns></returns>
public static int GetTicket(User user, int eventId)
{
int freeTicket = 5; // Default ticket is 5
// Get user event data, if exists, get free ticket
if (user.EventInfo.TryGetValue(eventId, out var eventData))
{
freeTicket = eventData.FreeTicket;
}
// Get item ticket information and free ticket max
(ItemData itemTicket, int freeTicketMax) = GetItemTicket(user, eventId);
// Get remain item ticket
int remainItemTicket = itemTicket?.Count ?? 0;
// If free ticket is greater than free ticket max, set free ticket to free ticket max
if (freeTicket > freeTicketMax) freeTicket = freeTicketMax;
int dateDay = user.GetDateDay();
// If dateDay is greater than last day, update user free ticket and last day
if (dateDay > eventData.LastDay)
{
Logging.WriteLine($"GetTicket ResetFreeTicket DateDay: {dateDay}, LastDay: {eventData.LastDay}, FreeTicketMax: {freeTicketMax}", LogType.Debug);
freeTicket = freeTicketMax;
user.EventInfo[eventId].FreeTicket = freeTicket;
user.EventInfo[eventId].LastDay = dateDay;
}
// Remain ticket is free ticket + item ticket
int remainTicket = freeTicket + remainItemTicket;
Logging.WriteLine($"GetTicket FreeTicket: {freeTicket}, ItemTicket: {remainItemTicket}, RemainTicket: {remainTicket}", LogType.Debug);
return remainTicket;
}
/// <summary>
/// Subtract user remaining ticket by event id and value
/// </summary>
/// <param name="user"></param>
/// <param name="eventId"></param>
/// <param name="val"></param>
/// <returns>remaining ticket</returns>
public static int SubtractTicket(User user, int eventId, int val)
{
int freeTicket = 5; // Default ticket is 5
// Get user event data, if exists, get free ticket
if (user.EventInfo.TryGetValue(eventId, out var eventData))
{
freeTicket = eventData.FreeTicket;
}
// Get item ticket information
(ItemData itemTicket, _) = GetItemTicket(user, eventId);
// Get remain item ticket
int remainItemTicket = itemTicket?.Count ?? 0;
// If free ticket is enough to subtract
if (freeTicket >= val)
{
freeTicket -= val;
user.EventInfo[eventId].FreeTicket = freeTicket;
int remainTicket = freeTicket + remainItemTicket;
Logging.WriteLine($"SubtractTicket Value: {val}, FreeTicket: {freeTicket}, ItemTicket: {remainItemTicket}, RemainTicket: {remainTicket}", LogType.Debug);
return remainTicket;
}
else
{
// If free ticket is not enough to subtract, subtract free ticket and subtract item ticket
int SubtractItemTicket = val - freeTicket;
user.EventInfo[eventId].FreeTicket = 0;
if (itemTicket is not null)
{
user.RemoveItemBySerialNumber(itemTicket.Isn, SubtractItemTicket);
}
freeTicket = 0;
// Remain ticket is free ticket + item ticket
int remainTicket = freeTicket + remainItemTicket;
Logging.WriteLine($"SubtractTicket Value: {val}, FreeTicket: {freeTicket}, ItemTicket: {remainItemTicket}, RemainTicket: {remainTicket}", LogType.Debug);
return remainTicket;
}
}
/// <summary>
/// Get user item ticket and free ticket max by event id
/// </summary>
/// <param name="user"></param>
/// <param name="eventId"></param>
/// <returns></returns>
private static (ItemData itemTicket, int freeTicketMax) GetItemTicket(User user, int eventId)
{
int freeTicketMax = 5; // Default free ticket max is 5
// Get event story data
var eventStory = GameData.Instance.EventStoryTable.Values.FirstOrDefault(x => x.EventId == eventId);
// If event story data is null or auto charge id is 0, return null and default free ticket max
if (eventStory is null || eventStory.AutoChargeId == 0) return (null, freeTicketMax);
log.Debug($"GetItemTicket EventId: {eventId}, EventStory: {JsonConvert.SerializeObject(eventStory)}");
// If auto charge data is null, return null and default free ticket max
if (!GameData.Instance.AutoChargeTable.TryGetValue(eventStory.AutoChargeId, out var autoCharge)) return (null, 5);
log.Debug($"GetItemTicket AutoChargeId: {eventStory.AutoChargeId}, AutoCharge: {JsonConvert.SerializeObject(autoCharge)}");
// If auto charge max is 0, return null and default free ticket max
if (autoCharge.AutoChargeMax == 0) return (null, freeTicketMax);
freeTicketMax = autoCharge.AutoChargeMax; // Set free ticket max to auto charge max
// Get user item
var userItem = user.Items.FirstOrDefault(x => x.ItemType == autoCharge.ItemId);
log.Debug($"GetItemTicket UserItem: {(userItem is not null ? JsonConvert.SerializeObject(userItem) : null)}");
return (userItem, freeTicketMax); // Return user item and free ticket max
}
}
}

View File

@@ -0,0 +1,39 @@
using EpinelPS.Utils;
using EpinelPS.Database;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Event.StoryEvent
{
[PacketPath("/event/storydungeon/fastclearstage")]
public class FastClearEventStage : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqFastClearEventStage req = await ReadData<ReqFastClearEventStage>();
User user = GetUser();
ResFastClearEventStage response = new();
NetRewardData reward = new();
NetRewardData bonusReward = new();
ClearEventStageHelper.ClearStage(user, req.StageId, ref reward, ref bonusReward, 1, req.ClearCount); // always battleResult = 1 for fast clear
user.AddTrigger(Trigger.EventDungeonStageClear, req.ClearCount, req.EventId);
response.RemainTicket = EventStoryHelper.SubtractTicket(user, req.EventId, req.ClearCount);
if (bonusReward.Item.Count > 0)
{
bonusReward.Item.ToList().ForEach(item =>
{
user.AddTrigger(Trigger.ObtainEventCurrencyMaterial, item.Count, item.Tid);
});
}
response.Reward = reward;
response.BonusReward = bonusReward;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -1,5 +1,6 @@
using EpinelPS.Utils;
using EpinelPS.Database;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Event.StoryEvent
{
@@ -9,32 +10,43 @@ namespace EpinelPS.LobbyServer.Event.StoryEvent
protected override async Task HandleAsync()
{
ReqStoryDungeonEventData req = await ReadData<ReqStoryDungeonEventData>();
int evId = req.EventId;
User user = GetUser();
if (!user.EventInfo.TryGetValue(evId, out EventData? eventData))
// Get user event data, if not exist, create new one
if (!user.EventInfo.TryGetValue(req.EventId, out EventData? eventData))
{
eventData = new();
eventData = new() { LastDay = user.GetDateDay(), FreeTicket = 5};
user.EventInfo.TryAdd(req.EventId, eventData);
}
ResStoryDungeonEventData response = new()
{
RemainTicket = 5,
TeamData = new NetUserTeamData()
{
LastContentsTeamNumber = 1,
Type = 20
}
RemainTicket = EventStoryHelper.GetTicket(user, req.EventId),
TeamData = new NetUserTeamData
{
Type = (int)TeamType.StoryEvent
},
};
if (user.UserTeams.TryGetValue((int)TeamType.StoryEvent, out NetUserTeamData? teamData))
{
response.TeamData = teamData;
}
foreach (var stageId in eventData.ClearedStages)
{
response.LastClearedEventStageList.Add(new NetLastClearedEventStageData()
{
StageId = stageId
});
}
response.LastClearedEventStageList.Add(new NetLastClearedEventStageData()
{
DifficultyId = eventData.Diff,
StageId = eventData.LastStage
});
// TOOD
// TODO
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -30,7 +30,7 @@ namespace EpinelPS.LobbyServer.FavoriteItem
if (req.ItemData == null)
{
throw new BadHttpRequestException($"No material item provIded", 400);
throw new BadHttpRequestException($"No material item provided", 400);
}
ItemData? userItem = user.Items.FirstOrDefault(x => x.Isn == req.ItemData.Isn);
@@ -59,7 +59,7 @@ namespace EpinelPS.LobbyServer.FavoriteItem
if (isGreatSuccess)
{
targetLevel = probabilityData.GreatSuccessRate;
targetLevel = probabilityData.GreatSuccessLevel;
}
int goldCost = baseExp * 10;

View File

@@ -20,13 +20,13 @@ namespace EpinelPS.LobbyServer.FavoriteItem
throw new BadHttpRequestException("Favorite item not found", 400);
}
int srItemTId = rFavoriteItem.Tid + 1;
int srItemTid = rFavoriteItem.Tid + 1;
NetUserFavoriteItemData? srInventoryItem = user.FavoriteItems.FirstOrDefault(f => f.Tid == srItemTId && f.Csn == 0);
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);
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);
@@ -43,7 +43,7 @@ namespace EpinelPS.LobbyServer.FavoriteItem
NetUserFavoriteItemData newSrFavoriteItem = new NetUserFavoriteItemData
{
FavoriteItemId = user.GenerateUniqueItemId(),
Tid = srItemTId,
Tid = srItemTid,
Csn = equippedCharacterCsn, // Maintain equipment status
Lv = expConversion.NewLevel,
Exp = expConversion.RemainingExp

View File

@@ -0,0 +1,19 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Hexacode;
[PacketPath("/hexacode/get-all")]
public class GetAll : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetHexaAll req = await ReadData<ReqGetHexaAll>();
User user = GetUser();
ResGetHexaAll response = new();
// TODO
await WriteDataAsync(response);
}
}

View File

@@ -14,9 +14,8 @@ namespace EpinelPS.LobbyServer.Intercept
ResInterceptAnomalousData response = new()
{
InterceptAnomalousManagerId = 101,
RemainingTickets = 5
TodayRemainingTickets = 5
};
response.ClearedInterceptAnomalousIds.Add([1, 2, 3, 4, 5]);
await WriteDataAsync(response);
}
}

View File

@@ -1,5 +1,6 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Intercept
{
@@ -8,17 +9,32 @@ namespace EpinelPS.LobbyServer.Intercept
{
protected override async Task HandleAsync()
{
ReqGetInterceptData req = await ReadData<ReqGetInterceptData>();
ReqGetInterceptData req = await ReadData<ReqGetInterceptData>();
int specialId = GetCurrentInterceptionIds();
ResGetInterceptData response = new()
{
NormalInterceptGroup = 1,
SpecialInterceptId = 1, // TODO switch this out each reset
SpecialInterceptId = specialId,
TicketCount = User.ResetableData.InterceptionTickets,
MaxTicketCount = JsonDb.Instance.MaxInterceptionCount
};
await WriteDataAsync(response);
}
private int GetCurrentInterceptionIds()
{
var specialTable = GameData.Instance.InterceptSpecial;
var specialBosses = specialTable.Values.Where(x => x.Group == 1).OrderBy(x => x.Order).ToList();
var dayOfYear = DateTime.UtcNow.DayOfYear;
var specialIndex = dayOfYear % specialBosses.Count;
var specialId = specialBosses[specialIndex].Id;
return specialId;
}
}
}

View File

@@ -0,0 +1,60 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/allclearequipment")]
public class AllClearEquipment : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqAllClearEquipment req = await ReadData<ReqAllClearEquipment>();
User user = GetUser();
ResAllClearEquipment response = new()
{
Csn = req.Csn
};
foreach (ItemData item in user.Items.ToArray())
{
if (item.Csn == req.Csn)
{
// Check if the item being unequipped is T10
if (IsT10Equipment(item.ItemType))
{
response.Items.Add(NetUtils.ToNet(item));
continue;
}
item.Csn = 0;
item.Position = 0;
response.Items.Add(NetUtils.ToNet(item));
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
private bool IsT10Equipment(int itemTypeId)
{
// Equipment ID format: 3 + Slot(1Head2Body3Arm4Leg) + Class(1Attacker2Defender3Supporter) + Rarity(01-09 T1-T9, 10 T10) + 01
// T10 equipment has rarity "10" in positions 3-4 (0-based indexing) for 7-digit IDs
string itemTypeStr = itemTypeId.ToString();
// Check if this is an equipment item (starts with 3) and has 7 digits
if (itemTypeStr.Length == 7 && itemTypeStr[0] == '3')
{
// Extract the rarity part (positions 3-4)
string rarityPart = itemTypeStr.Substring(3, 2);
return rarityPart == "10";
}
return false;
}
}
}

View File

@@ -1,35 +0,0 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/allclearequipment")]
public class ClearAllEquipment : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqAllClearEquipment req = await ReadData<ReqAllClearEquipment>();
User user = GetUser();
ResAllClearEquipment response = new()
{
Csn = req.Csn
};
foreach (ItemData item in user.Items.ToArray())
{
if (item.Csn == req.Csn)
{
// update character Id
item.Csn = 0;
response.Items.Add(NetUtils.ToNet(item));
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -1,5 +1,5 @@
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/get")]
@@ -13,9 +13,35 @@ namespace EpinelPS.LobbyServer.Inventory
ResGetInventoryData response = new();
foreach (ItemData item in user.Items)
{
ItemSubType itemSubType = GameData.Instance.GetItemSubType(item.ItemType);
if (itemSubType == ItemSubType.HarmonyCube)
{
NetUserHarmonyCubeData harmonyCubeData = new NetUserHarmonyCubeData()
{
Tid = item.ItemType,
Lv = item.Level,
Isn = item.Isn
};
harmonyCubeData.CsnList.AddRange(item.CsnList);
response.HarmonyCubes.Add(harmonyCubeData);
}
response.Items.Add(new NetUserItemData() { Count = item.Count, Tid = item.ItemType, Csn = item.Csn, Lv = item.Level, Exp = item.Exp, Corporation = item.Corp, Isn = item.Isn, Position = item.Position });
}
// TODO: HarmonyCubes, RunAwakeningIsnList, UserRedeems
// Add all equipment awakenings
foreach (EquipmentAwakeningData awakening in user.EquipmentAwakenings)
{
response.Awakenings.Add(new NetEquipmentAwakening()
{
Isn = awakening.Isn,
Option = awakening.Option
});
}
// TODO: UserRedeems
// Note: HarmonyCubes are now included in the Items list above
await WriteDataAsync(response);
}

View File

@@ -60,6 +60,9 @@ namespace EpinelPS.LobbyServer.Inventory
// we NEED to make sure the target item itself is in the delta list, or the UI won't update!
response.Items.Add(NetUtils.ToNet(destItem));
// Add trigger for equipment level count - 升级装备次数
user.AddTrigger(Trigger.EquipItemLevelCount, 1);
JsonDb.Save();
await WriteDataAsync(response);

View File

@@ -1,5 +1,7 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Data;
using System.Linq;
namespace EpinelPS.LobbyServer.Inventory
{
@@ -13,27 +15,120 @@ namespace EpinelPS.LobbyServer.Inventory
ResWearEquipmentList response = new();
// TODO optimize
foreach (long item2 in req.IsnList)
{
int pos = NetUtils.GetItemPos(user, item2);
// Check if the item being equipped is T10
ItemData? itemToCheck = user.Items.FirstOrDefault(x => x.Isn == item2);
if (itemToCheck != null && IsT10Equipment(itemToCheck.ItemType))
{
// If trying to equip a T10 item, check if there's already a T10 item in that position
bool hasT10InPosition = user.Items.Any(x => x.Position == pos && x.Csn == req.Csn && IsT10Equipment(x.ItemType));
if (hasT10InPosition)
{
// Don't allow replacing T10 equipment
continue;
}
}
// Check if item still exists after previous operations
itemToCheck = user.Items.FirstOrDefault(x => x.Isn == item2);
if (itemToCheck == null)
{
// Item no longer exists, skip this iteration
continue;
}
// unequip previous items
foreach (ItemData item in user.Items.ToArray())
{
if (item.Position == pos && item.Csn == req.Csn)
{
// Check if the item being unequipped is T10
if (IsT10Equipment(item.ItemType))
{
continue;
}
item.Csn = 0;
item.Position = 0;
response.Items.Add(NetUtils.ToNet(item));
}
}
foreach (ItemData item in user.Items.ToArray())
// Find the item to equip
ItemData? targetItem = user.Items.FirstOrDefault(x => x.Isn == item2);
if (targetItem != null)
{
if (item2 == item.Isn)
// Handle case where we have multiple copies of the same item
ItemData? equippedItem = null;
if (targetItem.Count > 1)
{
// Reduce count of original item
targetItem.Count--;
response.Items.Add(NetUtils.ToNet(targetItem));
// Create a new item instance to equip
equippedItem = new ItemData
{
ItemType = targetItem.ItemType,
Isn = user.GenerateUniqueItemId(),
Level = targetItem.Level,
Exp = targetItem.Exp,
Count = 1,
Corp = targetItem.Corp,
Position = pos // Set the position for the new item
};
user.Items.Add(equippedItem);
}
else
{
// Use the existing item
equippedItem = targetItem;
}
// equip the item
equippedItem.Csn = req.Csn;
equippedItem.Position = pos;
response.Items.Add(NetUtils.ToNet(equippedItem));
}
}
// Ensure all requested items are in the response
// This helps the client track the specific items that were requested
foreach (long requestedIsn in req.IsnList)
{
bool requestedItemAdded = response.Items.Any(x => x.Isn == requestedIsn);
if (!requestedItemAdded)
{
ItemData? requestedItem = user.Items.FirstOrDefault(x => x.Isn == requestedIsn);
if (requestedItem != null)
{
response.Items.Add(NetUtils.ToNet(requestedItem));
}
else
{
// If item not found, add it with count 0 to indicate it was processed
response.Items.Add(new NetUserItemData()
{
Isn = requestedIsn,
Count = 0
});
}
}
}
// Add all other equipped items for this character to the response
// This helps the client synchronize the full equipment state
foreach (ItemData item in user.Items)
{
if (item.Csn == req.Csn && item.Csn != 0)
{
// Check if this item was already added in the loop above
bool alreadyAdded = response.Items.Any(x => x.Isn == item.Isn);
if (!alreadyAdded)
{
item.Csn = req.Csn;
item.Position = pos;
response.Items.Add(NetUtils.ToNet(item));
}
}
@@ -43,5 +138,22 @@ namespace EpinelPS.LobbyServer.Inventory
await WriteDataAsync(response);
}
private bool IsT10Equipment(int itemTypeId)
{
// Equipment ID format: 3 + Slot(1Head2Body3Arm4Leg) + Class(1Attacker2Defender3Supporter) + Rarity(01-09 T1-T9, 10 T10) + 01
// T10 equipment has rarity "10" in positions 3-4 (0-based indexing) for 7-digit IDs
string itemTypeStr = itemTypeId.ToString();
// Check if this is an equipment item (starts with 3) and has 7 digits
if (itemTypeStr.Length == 7 && itemTypeStr[0] == '3')
{
// Extract the rarity part (positions 3-4)
string rarityPart = itemTypeStr.Substring(3, 2);
return rarityPart == "10";
}
return false;
}
}
}

View File

@@ -0,0 +1,350 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/equipment/awakening")]
public class AwakeningEquipment : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqEquipmentAwakening req = await ReadData<ReqEquipmentAwakening>();
User user = GetUser();
ResEquipmentAwakening response = new();
ItemData? equipmentToAwaken = user.Items.FirstOrDefault(x => x.Isn == req.Isn);
if (equipmentToAwaken == null)
{
await WriteDataAsync(response);
return;
}
int materialCost = 1;
int materialId = 7080001; // Equipment option material
ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId);
if (material == null || material.Count < materialCost)
{
await WriteDataAsync(response);
return;
}
if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items))
{
await WriteDataAsync(response);
return;
}
int awakenedEquipmentTypeId = GetAwakenedEquipmentTypeId(equipmentToAwaken.ItemType);
equipmentToAwaken.ItemType = awakenedEquipmentTypeId;
equipmentToAwaken.Level = 0;
equipmentToAwaken.Exp = 0;
equipmentToAwaken.Corp = 0;
Random rng = new Random();
NetEquipmentAwakeningOption awakeningOption = new NetEquipmentAwakeningOption()
{
Option1Lock = false,
IsOption1DisposableLock = false,
Option2Lock = false,
IsOption2DisposableLock = false,
Option3Lock = false,
IsOption3DisposableLock = false
};
if (GameData.Instance.ItemEquipTable.TryGetValue(awakenedEquipmentTypeId, out ItemEquipRecord? equipRecord))
{
try
{
GenerateAwakeningOptions(awakeningOption, equipRecord, rng);
}
catch (InvalidOperationException ex)
{
Logging.WriteLine($"Failed to generate awakening options: {ex.Message}", LogType.Error);
await WriteDataAsync(response);
return;
}
}
user.EquipmentAwakenings.Add(new EquipmentAwakeningData()
{
Isn = equipmentToAwaken.Isn,
Option = awakeningOption,
IsNewData = false
});
response.Awakening = new NetEquipmentAwakening()
{
Isn = equipmentToAwaken.Isn,
Option = awakeningOption
};
response.Items.Add(NetUtils.ToNet(equipmentToAwaken));
JsonDb.Save();
await WriteDataAsync(response);
}
private int GetAwakenedEquipmentTypeId(int originalTypeId)
{
// Equipment ID format: 3 + Slot(1Head2Body3Arm4Leg) + Class(1Attacker2Defender3Supporter) + Rarity(01-09 T1-T9, 10 T10) + 01
// Awakening changes T9 equipment (09) to T10 equipment (10)
return originalTypeId switch
{
// Attacker equipment awakening
3110901 => 3111001, // Head T9 -> T10
3210901 => 3211001, // Body T9 -> T10
3310901 => 3311001, // Arm T9 -> T10
3410901 => 3411001, // Leg T9 -> T10
// Defender equipment awakening
3120901 => 3121001, // Head T9 -> T10
3220901 => 3221001, // Body T9 -> T10
3320901 => 3321001, // Arm T9 -> T10
3420901 => 3421001, // Leg T9 -> T10
// Supporter equipment awakening
3130901 => 3131001, // Head T9 -> T10
3230901 => 3231001, // Body T9 -> T10
3330901 => 3331001, // Arm T9 -> T10
3430901 => 3431001, // Leg T9 -> T10
// Default return original ID (awakening not supported)
_ => originalTypeId
};
}
private void GenerateAwakeningOptions(NetEquipmentAwakeningOption option, ItemEquipRecord equipRecord, Random rng)
{
List<int> excludedStateEffectIds = new List<int>();
// 1.0 = 100% chance for slot 1, 0.5 = 50% chance for slot 2, 0.3 = 30% chance for slot 3
double[] slotActivationProbabilities = { 1.0, 0.5, 0.3 };
for (int i = 1; i <= 3; i++)
{
bool shouldActivateSlot = rng.NextDouble() < slotActivationProbabilities[i - 1];
if (shouldActivateSlot)
{
int selectedOptionId = GenerateNewOptionIdInit(excludedStateEffectIds, 11);
AddOptionToExclusionList(selectedOptionId, excludedStateEffectIds);
switch (i)
{
case 1:
option.Option1Id = selectedOptionId;
break;
case 2:
option.Option2Id = selectedOptionId;
break;
case 3:
option.Option3Id = selectedOptionId;
break;
}
}
else
{
switch (i)
{
case 1:
option.Option1Id = 0;
break;
case 2:
option.Option2Id = 0;
break;
case 3:
option.Option3Id = 0;
break;
}
}
}
option.Option1Lock = false;
option.IsOption1DisposableLock = false;
option.Option2Lock = false;
option.IsOption2DisposableLock = false;
option.Option3Lock = false;
option.IsOption3DisposableLock = false;
}
private int GenerateNewOptionIdInit(List<int> excludedStateEffectIds, int level)
{
List<EquipmentOptionRecord> allAwakeningOptions = GameData.Instance.EquipmentOptionTable.Values
.Where(x => x.EquipmentOptionGroupId == 100000 &&
x.StateEffectList?.Any(se => se.StateEffectLevel == level) == true)
.ToList();
HashSet<int> excludedEffectGroupIds = new HashSet<int>();
foreach (int stateEffectId in excludedStateEffectIds)
{
EquipmentOptionRecord? excludedOption = GameData.Instance.EquipmentOptionTable.Values
.FirstOrDefault(opt => opt.StateEffectList != null && opt.StateEffectList.Any(se => se.StateEffectId == stateEffectId));
if (excludedOption != null)
{
excludedEffectGroupIds.Add(excludedOption.StateEffectGroupId);
}
}
List<EquipmentOptionRecord> availableOptions = allAwakeningOptions
.Where(option => !excludedEffectGroupIds.Contains(option.StateEffectGroupId))
.ToList();
Dictionary<int, List<EquipmentOptionRecord>> optionsByEffectGroup = availableOptions
.GroupBy(option => option.StateEffectGroupId)
.ToDictionary(g => g.Key, g => g.ToList());
if (optionsByEffectGroup.Count == 0)
{
throw new InvalidOperationException("No available equipment options for awakening - this indicates a data consistency issue");
}
double excludedProbabilitySum = CalculateExcludedProbabilitySumByEffectGroup(excludedEffectGroupIds, level);
List<EffectGroupWithWeight> weightedEffectGroups = CalculateDynamicProbabilitiesForEffectGroups(optionsByEffectGroup, excludedProbabilitySum);
int selectedEffectGroupId = SelectWeightedRandomEffectGroup(weightedEffectGroups);
List<EquipmentOptionRecord> optionsInSelectedGroup = optionsByEffectGroup[selectedEffectGroupId];
foreach (EquipmentOptionRecord option in optionsInSelectedGroup)
{
StateEffectList? stateEffect = option.StateEffectList?.FirstOrDefault(se => se.StateEffectLevel == level);
if (stateEffect != null)
{
return stateEffect.StateEffectId;
}
}
throw new InvalidOperationException($"No state effect found with level {level} in selected effect group - this indicates a data consistency issue");
}
/// <summary>
/// Calculates the sum of base probabilities for excluded effect groups according to the Overload system rules
/// </summary>
/// <param name="excludedEffectGroupIds">List of excluded state_effect_group_ids</param>
/// <param name="level">The level of options to consider</param>
/// <returns>Sum of base probabilities (as decimal percentage)</returns>
private double CalculateExcludedProbabilitySumByEffectGroup(HashSet<int> excludedEffectGroupIds, int level)
{
if (excludedEffectGroupIds.Count == 0)
{
return 0.0;
}
double totalExcluded = 0.0;
foreach (int effectGroupId in excludedEffectGroupIds)
{
List<EquipmentOptionRecord> options = GameData.Instance.EquipmentOptionTable.Values
.Where(opt => opt.StateEffectGroupId == effectGroupId &&
opt.EquipmentOptionGroupId == 100000 &&
opt.StateEffectList?.Any(se => se.StateEffectLevel == level) == true)
.ToList();
if (options.Count > 0)
{
EquipmentOptionRecord firstOption = options.First();
totalExcluded += firstOption.OptionGroupRatio / 100.0;
}
}
return Math.Min(totalExcluded, 99.9); // Ensure we don't reach 100% to avoid division by zero
}
/// <summary>
/// Helper class to store effect group with its calculated weight for probability selection
/// </summary>
public class EffectGroupWithWeight
{
public int EffectGroupId { get; set; }
public double Weight { get; set; }
public double BaseProbability { get; set; }
public double DynamicProbability { get; set; }
}
/// <summary>
/// Calculates dynamic probabilities for available effect groups using the formula:
/// Dynamic Probability = Display Probability / (100% - Sum of Excluded Probabilities)
/// </summary>
/// <param name="optionsByEffectGroup">Dictionary of available options grouped by effect group ID</param>
/// <param name="excludedProbabilitySum">Sum of probabilities of excluded effects</param>
/// <returns>List of weighted effect groups for random selection</returns>
private List<EffectGroupWithWeight> CalculateDynamicProbabilitiesForEffectGroups(Dictionary<int, List<EquipmentOptionRecord>> optionsByEffectGroup, double excludedProbabilitySum)
{
List<EffectGroupWithWeight> weightedEffectGroups = new List<EffectGroupWithWeight>();
double probabilityDenominator = 100.0 - excludedProbabilitySum;
if (probabilityDenominator <= 0)
{
probabilityDenominator = 1.0;
}
foreach (KeyValuePair<int, List<EquipmentOptionRecord>> kvp in optionsByEffectGroup)
{
int effectGroupId = kvp.Key;
List<EquipmentOptionRecord> options = kvp.Value;
if (options.Count > 0)
{
EquipmentOptionRecord firstOption = options.First();
double baseProbability = (double)firstOption.OptionGroupRatio / 100.0;
double dynamicProbability = baseProbability / probabilityDenominator;
double selectionWeight = dynamicProbability * 1000000;
weightedEffectGroups.Add(new EffectGroupWithWeight
{
EffectGroupId = effectGroupId,
Weight = selectionWeight,
BaseProbability = baseProbability,
DynamicProbability = dynamicProbability
});
}
}
return weightedEffectGroups;
}
/// <summary>
/// Selects an effect group randomly based on calculated weights
/// </summary>
/// <param name="weightedEffectGroups">List of weighted effect groups</param>
/// <returns>Selected effect_group_id</returns>
private int SelectWeightedRandomEffectGroup(List<EffectGroupWithWeight> weightedEffectGroups)
{
Random random = new Random();
double totalWeight = weightedEffectGroups.Sum(weg => weg.Weight);
double randomValue = random.NextDouble() * totalWeight;
double cumulativeWeight = 0.0;
foreach (EffectGroupWithWeight weightedGroup in weightedEffectGroups)
{
cumulativeWeight += weightedGroup.Weight;
if (randomValue <= cumulativeWeight)
{
return weightedGroup.EffectGroupId;
}
}
return weightedEffectGroups.Last().EffectGroupId;
}
private void AddOptionToExclusionList(int optionId, List<int> excludedStateEffectIds)
{
if (!excludedStateEffectIds.Contains(optionId))
{
excludedStateEffectIds.Add(optionId);
}
}
}
}

View File

@@ -0,0 +1,117 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/equipment/changeoption")]
public class ChangeOption : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqAwakeningChangeOption req = await ReadData<ReqAwakeningChangeOption>();
User user = GetUser();
ResAwakeningChangeOption response = new ResAwakeningChangeOption();
if (req.Isn <= 0)
{
await WriteDataAsync(response);
return;
}
EquipmentAwakeningData? oldAwakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn && !x.IsNewData);
if (oldAwakening == null)
{
oldAwakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn);
await WriteDataAsync(response);
return;
}
List<EquipmentAwakeningData> duplicates = user.EquipmentAwakenings.Where(x => x.Isn == req.Isn).ToList();
if (req.IsChanged)
{
if (duplicates.Count > 1)
{
List<EquipmentAwakeningData> oldEntries = duplicates.Where(x => !x.IsNewData).ToList();
foreach (EquipmentAwakeningData oldEntry in oldEntries)
{
user.EquipmentAwakenings.Remove(oldEntry);
}
EquipmentAwakeningData? newEntry = duplicates.FirstOrDefault(x => x.IsNewData);
if (newEntry != null)
{
newEntry.IsNewData = false;
}
}
else if (duplicates.Count == 1)
{
EquipmentAwakeningData singleEntry = duplicates[0];
if (singleEntry.IsNewData)
{
singleEntry.IsNewData = false;
}
}
EquipmentAwakeningData? confirmedAwakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn);
if (confirmedAwakening != null)
{
response.Awakening = new NetEquipmentAwakening()
{
Isn = confirmedAwakening.Isn,
Option = new NetEquipmentAwakeningOption()
{
Option1Id = confirmedAwakening.Option.Option1Id,
Option1Lock = confirmedAwakening.Option.Option1Lock,
IsOption1DisposableLock = confirmedAwakening.Option.IsOption1DisposableLock,
Option2Id = confirmedAwakening.Option.Option2Id,
Option2Lock = confirmedAwakening.Option.Option2Lock,
IsOption2DisposableLock = confirmedAwakening.Option.IsOption2DisposableLock,
Option3Id = confirmedAwakening.Option.Option3Id,
Option3Lock = confirmedAwakening.Option.Option3Lock,
IsOption3DisposableLock = confirmedAwakening.Option.IsOption3DisposableLock
}
};
}
}
else
{
if (duplicates.Count > 1)
{
List<EquipmentAwakeningData> newEntries = duplicates.Where(x => x.IsNewData).ToList();
foreach (EquipmentAwakeningData newEntry in newEntries)
{
user.EquipmentAwakenings.Remove(newEntry);
}
}
EquipmentAwakeningData? originalAwakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn);
if (originalAwakening != null)
{
response.Awakening = new NetEquipmentAwakening()
{
Isn = originalAwakening.Isn,
Option = new NetEquipmentAwakeningOption()
{
Option1Id = originalAwakening.Option.Option1Id,
Option1Lock = originalAwakening.Option.Option1Lock,
IsOption1DisposableLock = originalAwakening.Option.IsOption1DisposableLock,
Option2Id = originalAwakening.Option.Option2Id,
Option2Lock = originalAwakening.Option.Option2Lock,
IsOption2DisposableLock = originalAwakening.Option.IsOption2DisposableLock,
Option3Id = originalAwakening.Option.Option3Id,
Option3Lock = originalAwakening.Option.Option3Lock,
IsOption3DisposableLock = originalAwakening.Option.IsOption3DisposableLock
}
};
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,174 @@
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/equipment/getawakeningdetail")]
public class GetAwakeningDetail : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetAwakeningDetail req = await ReadData<ReqGetAwakeningDetail>();
User user = GetUser();
ResGetAwakeningDetail response = new ResGetAwakeningDetail();
// Validate input parameters
if (req.Isn <= 0)
{
await WriteDataAsync(response);
return;
}
// Find the equipment awakening data
EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn);
if (awakening == null)
{
await WriteDataAsync(response);
return;
}
// Set current options
response.CurrentOption = new NetEquipmentAwakeningOption()
{
Option1Id = awakening.Option.Option1Id,
Option1Lock = awakening.Option.Option1Lock,
IsOption1DisposableLock = awakening.Option.IsOption1DisposableLock,
Option2Id = awakening.Option.Option2Id,
Option2Lock = awakening.Option.Option2Lock,
IsOption2DisposableLock = awakening.Option.IsOption2DisposableLock,
Option3Id = awakening.Option.Option3Id,
Option3Lock = awakening.Option.Option3Lock,
IsOption3DisposableLock = awakening.Option.IsOption3DisposableLock
};
NetEquipmentAwakeningOption newOption = new NetEquipmentAwakeningOption();
// Process each option slot (1, 2, 3)
for (int i = 1; i <= 3; i++)
{
// Get current option ID for this slot
int currentOptionId = GetOptionIdForSlot(awakening.Option, i);
bool isLocked = IsOptionLocked(awakening.Option, i);
bool isDisposableLocked = IsOptionDisposableLocked(awakening.Option, i);
// If option is permanently locked or disposable locked, keep it unchanged
if (isLocked || isDisposableLocked)
{
// Keep the current option unchanged
SetOptionForSlot(newOption, i, currentOptionId, isLocked, isDisposableLocked);
continue;
}
// If not locked, generate a new option
int newOptionId = GenerateNewOptionId(currentOptionId);
SetOptionForSlot(newOption, i, newOptionId, false, false);
}
response.NewOption = newOption;
await WriteDataAsync(response);
}
private int GetOptionIdForSlot(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.Option1Id,
2 => option.Option2Id,
3 => option.Option3Id,
_ => 0
};
}
private bool IsOptionLocked(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.Option1Lock,
2 => option.Option2Lock,
3 => option.Option3Lock,
_ => false
};
}
private bool IsOptionDisposableLocked(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.IsOption1DisposableLock,
2 => option.IsOption2DisposableLock,
3 => option.IsOption3DisposableLock,
_ => false
};
}
private void SetOptionForSlot(NetEquipmentAwakeningOption option, int slot, int optionId, bool locked, bool disposableLocked)
{
switch (slot)
{
case 1:
option.Option1Id = optionId;
option.Option1Lock = locked;
option.IsOption1DisposableLock = disposableLocked;
break;
case 2:
option.Option2Id = optionId;
option.Option2Lock = locked;
option.IsOption2DisposableLock = disposableLocked;
break;
case 3:
option.Option3Id = optionId;
option.Option3Lock = locked;
option.IsOption3DisposableLock = disposableLocked;
break;
}
}
private int GenerateNewOptionId(int currentOptionId)
{
// Get the current option record
if (!GameData.Instance.EquipmentOptionTable.TryGetValue(currentOptionId, out EquipmentOptionRecord? currentOption))
{
return currentOptionId;
}
// Get the group ID of the current option
int groupId = currentOption.EquipmentOptionGroupId;
// Find all options in the same group
List<EquipmentOptionRecord> optionsInGroup = GameData.Instance.EquipmentOptionTable.Values
.Where(x => x.EquipmentOptionGroupId == groupId)
.ToList();
if (optionsInGroup.Count == 0)
{
return currentOptionId;
}
// Calculate total ratio for probability calculation
long totalRatio = optionsInGroup.Sum(x => (long)x.OptionRatio);
if (totalRatio == 0)
{
return currentOptionId;
}
// Select a new option based on probability
Random random = new Random();
long randomValue = random.NextInt64(0, totalRatio);
long cumulativeRatio = 0;
foreach (EquipmentOptionRecord option in optionsInGroup)
{
cumulativeRatio += option.OptionRatio;
if (randomValue < cumulativeRatio)
{
return option.Id;
}
}
return currentOptionId;
}
}
}

View File

@@ -0,0 +1,137 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/equipment/lockoption")]
public class LockOption : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqAwakeningLockOption req = await ReadData<ReqAwakeningLockOption>();
User user = GetUser();
ResAwakeningLockOption response = new ResAwakeningLockOption();
EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn);
if (awakening == null)
{
await WriteDataAsync(response);
return;
}
int slot = 0;
if (awakening.Option.Option1Id == req.OptionId)
slot = 1;
else if (awakening.Option.Option2Id == req.OptionId)
slot = 2;
else if (awakening.Option.Option3Id == req.OptionId)
slot = 3;
(int materialId, int materialCost) = GetMaterialInfoForAwakening(awakening.Option);
ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId);
if (material == null || material.Count < materialCost)
{
await WriteDataAsync(response);
return;
}
UpdateLockStatus(awakening.Option, slot, req.IsLocked);
if (req.IsLocked)
{
if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items))
{
await WriteDataAsync(response);
return;
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
private static int CalculateMaterialCost(NetEquipmentAwakeningOption option)
{
int lockedOptionCount = 0;
int disposableLockOptionCount = 0;
// Count already permanently locked options (not disposable locks)
if (option.Option1Id != 0 && option.Option1Lock && !option.IsOption1DisposableLock)
lockedOptionCount++;
if (option.Option2Id != 0 && option.Option2Lock && !option.IsOption2DisposableLock)
lockedOptionCount++;
if (option.Option3Id != 0 && option.Option3Lock && !option.IsOption3DisposableLock)
lockedOptionCount++;
if (option.Option1Id != 0 && option.Option1Lock && option.IsOption1DisposableLock)
disposableLockOptionCount++;
if (option.Option2Id != 0 && option.Option2Lock && option.IsOption2DisposableLock)
disposableLockOptionCount++;
if (option.Option3Id != 0 && option.Option3Lock && option.IsOption3DisposableLock)
disposableLockOptionCount++;
return GetPermanentLockCostId(lockedOptionCount,disposableLockOptionCount);
}
private static int GetPermanentLockCostId(int lockedOptionCount,int disposableLockOptionCount)
{
// For permanent locks, use cost_group_id 100
EquipmentOptionCostRecord? costRecord = GameData.Instance.EquipmentOptionCostTable.Values
.FirstOrDefault(x => x.CostGroupId == 100 && x.CostLevel == lockedOptionCount && x.DisposableFixCostLevel == disposableLockOptionCount);
int costId = costRecord?.CostId ?? 101001;
return costId;
}
private static void UpdateLockStatus(NetEquipmentAwakeningOption option, int slot, bool isLocked)
{
switch (slot)
{
case 1:
option.Option1Lock = isLocked;
if (isLocked)
{
option.IsOption1DisposableLock = false;
}
break;
case 2:
option.Option2Lock = isLocked;
if (isLocked)
{
option.IsOption2DisposableLock = false;
}
break;
case 3:
option.Option3Lock = isLocked;
if (isLocked)
{
option.IsOption3DisposableLock = false;
}
break;
}
}
private static (int materialId, int materialCost) GetMaterialInfoForAwakening(NetEquipmentAwakeningOption option)
{
int costId = CalculateMaterialCost(option);
return GetMaterialInfo(costId);
}
private static (int materialId, int materialCost) GetMaterialInfo(int costId)
{
if (GameData.Instance.costTable.TryGetValue(costId, out CostRecord? costRecord) &&
costRecord?.Costs != null &&
costRecord.Costs.Count > 0)
{
return (costRecord.Costs[0].ItemId, costRecord.Costs[0].ItemValue);
}
return (7080001, 2); // Default material ID and cost
}
}
}

View File

@@ -0,0 +1,475 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/equipment/resetoption")]
public class ResetOption : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqAwakeningResetOption req = await ReadData<ReqAwakeningResetOption>();
User user = GetUser();
ResAwakeningResetOption response = new ResAwakeningResetOption();
EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn);
if (awakening == null)
{
await WriteDataAsync(response);
return;
}
NetEquipmentAwakeningOption resetOption = new NetEquipmentAwakeningOption();
Random random = new Random();
(int optionId, bool isLocked, bool isDisposableLocked)[] slotLockInfo = new (int optionId, bool isLocked, bool isDisposableLocked)[3];
List<int> lockedOptionStateEffectIds = new List<int>();
int lockedOptionCount = 0;
for (int i = 1; i <= 3; i++)
{
int currentOptionId = GetOptionIdForSlot(awakening.Option, i);
bool isLocked = IsOptionLocked(awakening.Option, i);
bool isDisposableLocked = IsOptionDisposableLocked(awakening.Option, i);
slotLockInfo[i - 1] = (currentOptionId, isLocked, isDisposableLocked);
// Count locked options for material cost calculation
if (isLocked || isDisposableLocked)
lockedOptionCount++;
// Collect locked options for exclusion list
if (isLocked && currentOptionId != 0)
{
AddOptionToExclusionList(currentOptionId, lockedOptionStateEffectIds);
}
}
int costId = GetCostIdByLockedOptionCount(lockedOptionCount);
(int materialId, int materialCost) = GetMaterialInfo(costId);
// Check if user has enough materials
ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId);
if (material == null || material.Count < materialCost)
{
Logging.WriteLine($"Insufficient materials for reset operation. Need {materialCost} of item {materialId}, but have {material?.Count ?? 0}", LogType.Warning);
await WriteDataAsync(response);
return;
}
// Deduct materials for reset
if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items))
{
Logging.WriteLine($"Insufficient materials for reset operation. Need {materialCost} of item {materialId}, but have {material?.Count ?? 0}", LogType.Warning);
await WriteDataAsync(response);
return;
}
// Process each option slot (1, 2, 3)
ProcessOptionSlots(awakening, resetOption, slotLockInfo, lockedOptionStateEffectIds);
// Create a new awakening entry with the same ISN to preserve the old data
EquipmentAwakeningData newAwakening = new EquipmentAwakeningData()
{
Isn = awakening.Isn,
Option = new NetEquipmentAwakeningOption()
{
Option1Id = resetOption.Option1Id,
Option1Lock = resetOption.Option1Lock,
IsOption1DisposableLock = resetOption.IsOption1DisposableLock,
Option2Id = resetOption.Option2Id,
Option2Lock = resetOption.Option2Lock,
IsOption2DisposableLock = resetOption.IsOption2DisposableLock,
Option3Id = resetOption.Option3Id,
Option3Lock = resetOption.Option3Lock,
IsOption3DisposableLock = resetOption.IsOption3DisposableLock
},
IsNewData = true
};
user.EquipmentAwakenings.Add(newAwakening);
// Add the reset options to the response
response.ResetOption = new NetEquipmentAwakeningOption()
{
Option1Id = resetOption.Option1Id,
Option1Lock = resetOption.Option1Lock,
IsOption1DisposableLock = resetOption.IsOption1DisposableLock,
Option2Id = resetOption.Option2Id,
Option2Lock = resetOption.Option2Lock,
IsOption2DisposableLock = resetOption.IsOption2DisposableLock,
Option3Id = resetOption.Option3Id,
Option3Lock = resetOption.Option3Lock,
IsOption3DisposableLock = resetOption.IsOption3DisposableLock
};
JsonDb.Save();
await WriteDataAsync(response);
}
private void ProcessOptionSlots(EquipmentAwakeningData awakening, NetEquipmentAwakeningOption resetOption, (int optionId, bool isLocked, bool isDisposableLocked)[] slotLockInfo, List<int> lockedOptionStateEffectIds)
{
Random random = new Random();
// 1.0 = 100% chance for slot 1, 0.5 = 50% chance for slot 2, 0.3 = 30% chance for slot 3
double[] slotActivationProbabilities = { 1.0, 0.5, 0.3 };
for (int i = 1; i <= 3; i++)
{
(int currentOptionId, bool isLocked, bool isDisposableLocked) = slotLockInfo[i - 1];
if (isLocked && !isDisposableLocked)
{
SetOptionForSlot(resetOption, i, currentOptionId, true, false);
}
else if (isLocked && isDisposableLocked)
{
SetOptionForSlot(resetOption, i, currentOptionId, false, false);
UnlockDisposableOption(awakening.Option, i);
}
else
{
bool shouldActivateSlot = random.NextDouble() < slotActivationProbabilities[i - 1];
if (shouldActivateSlot)
{
// Generate new option using non-repeating system with dynamic probability
int newOptionId = GenerateNewOptionIdWithDynamicProbability(lockedOptionStateEffectIds);
SetOptionForSlot(resetOption, i, newOptionId, false, false);
// Add the new option to locked list to prevent duplicates in subsequent slots
if (newOptionId != 0)
{
AddOptionToExclusionList(newOptionId, lockedOptionStateEffectIds);
}
}
else
{
SetOptionForSlot(resetOption, i, 0, false, false);
}
}
}
}
private void UnlockDisposableOption(NetEquipmentAwakeningOption option, int slot)
{
SetOptionForSlot(option, slot, GetOptionIdForSlot(option, slot), false, false);
}
private int GetCostIdByLockedOptionCount(int lockedOptionCount)
{
if (lockedOptionCount == 0)
{
return 100001;
}
EquipmentOptionCostRecord? costRecord = GameData.Instance.EquipmentOptionCostTable.Values
.FirstOrDefault(x => x.CostGroupId == 200 && x.CostLevel == lockedOptionCount);
return costRecord?.CostId ?? 100001;
}
private int GetOptionIdForSlot(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.Option1Id,
2 => option.Option2Id,
3 => option.Option3Id,
_ => 0
};
}
private bool IsOptionLocked(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.Option1Lock,
2 => option.Option2Lock,
3 => option.Option3Lock,
_ => false
};
}
private bool IsOptionDisposableLocked(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.IsOption1DisposableLock,
2 => option.IsOption2DisposableLock,
3 => option.IsOption3DisposableLock,
_ => false
};
}
private void SetOptionForSlot(NetEquipmentAwakeningOption option, int slot, int optionId, bool locked, bool disposableLocked)
{
switch (slot)
{
case 1:
option.Option1Id = optionId;
option.Option1Lock = locked;
option.IsOption1DisposableLock = disposableLocked;
break;
case 2:
option.Option2Id = optionId;
option.Option2Lock = locked;
option.IsOption2DisposableLock = disposableLocked;
break;
case 3:
option.Option3Id = optionId;
option.Option3Lock = locked;
option.IsOption3DisposableLock = disposableLocked;
break;
}
}
private void AddOptionToExclusionList(int optionId, List<int> lockedOptionStateEffectIds)
{
// Since optionId is already a state_effect_id, we can directly add it to the exclusion list
if (!lockedOptionStateEffectIds.Contains(optionId))
{
lockedOptionStateEffectIds.Add(optionId);
// Also get the effect group ID for this state_effect_id to exclude the effect group
EquipmentOptionRecord? optionRecord = GameData.Instance.EquipmentOptionTable.Values
.FirstOrDefault(opt => opt.StateEffectList != null && opt.StateEffectList.Any(se => se.StateEffectId == optionId));
}
}
/// <summary>
/// Generates a new option ID using the Overload system's non-repeating effect types and dynamic probability formula
/// </summary>
/// <param name="excludedStateEffectIds">List of state_effect_ids that are already taken and should be excluded</param>
/// <returns>A new state_effect_id or 0 if none available</returns>
private int GenerateNewOptionIdWithDynamicProbability(List<int> excludedStateEffectIds)
{
// Get all awakening options (equipment_option_group_id == 100000)
List<EquipmentOptionRecord> allAwakeningOptions = GameData.Instance.EquipmentOptionTable.Values
.Where(x => x.EquipmentOptionGroupId == 100000)
.ToList();
// Filter out options that have already been taken (non-repeating principle)
HashSet<int> excludedEffectGroupIds = new HashSet<int>();
// Get the effect group IDs for all excluded state effect IDs
foreach (int stateEffectId in excludedStateEffectIds)
{
EquipmentOptionRecord? excludedOption = GameData.Instance.EquipmentOptionTable.Values
.FirstOrDefault(opt => opt.StateEffectList != null && opt.StateEffectList.Any(se => se.StateEffectId == stateEffectId));
if (excludedOption != null)
{
excludedEffectGroupIds.Add(excludedOption.StateEffectGroupId);
}
}
List<EquipmentOptionRecord> availableOptions = allAwakeningOptions
.Where(option => !excludedEffectGroupIds.Contains(option.StateEffectGroupId))
.ToList();
Dictionary<int, List<EquipmentOptionRecord>> optionsByEffectGroup = availableOptions
.GroupBy(option => option.StateEffectGroupId)
.ToDictionary(g => g.Key, g => g.ToList());
double excludedProbabilitySum = CalculateExcludedProbabilitySumByEffectGroup(excludedEffectGroupIds);
List<EffectGroupWithWeight> weightedEffectGroups = CalculateDynamicProbabilitiesForEffectGroups(optionsByEffectGroup, excludedProbabilitySum);
int selectedEffectGroupId = SelectWeightedRandomEffectGroup(weightedEffectGroups);
List<EquipmentOptionRecord> optionsInSelectedGroup = optionsByEffectGroup[selectedEffectGroupId];
int selectedStateEffectId = SelectOptionFromGroup(optionsInSelectedGroup);
return selectedStateEffectId;
}
/// <summary>
/// Helper class to store effect group with its calculated weight for probability selection
/// </summary>
public class EffectGroupWithWeight
{
public int EffectGroupId { get; set; }
public double Weight { get; set; }
public double BaseProbability { get; set; }
public double DynamicProbability { get; set; }
}
/// <summary>
/// Calculates the sum of base probabilities for excluded effect groups according to the Overload system rules
/// </summary>
/// <param name="excludedEffectGroupIds">List of excluded state_effect_group_ids</param>
/// <returns>Sum of base probabilities (as decimal percentage)</returns>
private double CalculateExcludedProbabilitySumByEffectGroup(HashSet<int> excludedEffectGroupIds)
{
if (excludedEffectGroupIds.Count == 0)
{
return 0.0;
}
double totalExcluded = 0.0;
foreach (int effectGroupId in excludedEffectGroupIds)
{
List<EquipmentOptionRecord> options = GameData.Instance.EquipmentOptionTable.Values
.Where(opt => opt.StateEffectGroupId == effectGroupId && opt.EquipmentOptionGroupId == 100000)
.ToList();
if (options.Count > 0)
{
EquipmentOptionRecord firstOption = options.First();
totalExcluded += firstOption.OptionGroupRatio / 100.0;
}
}
// Cap the excluded probability sum to maintain mathematical validity
return Math.Min(totalExcluded, 99.9); // Ensure we don't reach 100% to avoid division by zero
}
/// <summary>
/// Calculates dynamic probabilities for available effect groups using the formula:
/// Dynamic Probability = Display Probability / (100% - Sum of Excluded Probabilities)
/// </summary>
/// <param name="optionsByEffectGroup">Dictionary of available options grouped by effect group ID</param>
/// <param name="excludedProbabilitySum">Sum of probabilities of excluded effects</param>
/// <returns>List of weighted effect groups for random selection</returns>
private List<EffectGroupWithWeight> CalculateDynamicProbabilitiesForEffectGroups(Dictionary<int, List<EquipmentOptionRecord>> optionsByEffectGroup, double excludedProbabilitySum)
{
List<EffectGroupWithWeight> weightedEffectGroups = new List<EffectGroupWithWeight>();
double probabilityDenominator = 100.0 - excludedProbabilitySum;
// Prevent division by zero or negative values (shouldn't happen due to capping in CalculateExcludedProbabilitySumByEffectGroup, but let's be safe)
if (probabilityDenominator <= 0)
{
Logging.WriteLine($"Warning: probabilityDenominator is {probabilityDenominator}, using uniform distribution", LogType.Warning);
probabilityDenominator = 1.0; // Use uniform distribution as fallback
}
foreach (KeyValuePair<int, List<EquipmentOptionRecord>> kvp in optionsByEffectGroup)
{
int effectGroupId = kvp.Key;
List<EquipmentOptionRecord> options = kvp.Value;
if (options.Count > 0)
{
EquipmentOptionRecord firstOption = options.First();
double baseProbability = firstOption.OptionGroupRatio / 100.0;
double dynamicProbability = baseProbability / probabilityDenominator;
double selectionWeight = dynamicProbability * 1000000;
weightedEffectGroups.Add(new EffectGroupWithWeight
{
EffectGroupId = effectGroupId,
Weight = selectionWeight,
BaseProbability = baseProbability,
DynamicProbability = dynamicProbability
});
}
}
return weightedEffectGroups;
}
/// <summary>
/// Selects an effect group randomly based on calculated weights
/// </summary>
/// <param name="weightedEffectGroups">List of weighted effect groups</param>
/// <returns>Selected effect_group_id or 0 if none available</returns>
private int SelectWeightedRandomEffectGroup(List<EffectGroupWithWeight> weightedEffectGroups)
{
// Safety check to prevent data corruption
if (weightedEffectGroups == null || weightedEffectGroups.Count == 0)
throw new InvalidOperationException("No weighted effect groups available - this indicates a data consistency issue");
Random random = new Random();
double totalWeight = weightedEffectGroups.Sum(weg => weg.Weight);
// Prevent division by zero which could cause unexpected behavior
if (totalWeight <= 0)
throw new InvalidOperationException("Invalid group weights - this indicates a data consistency issue");
double randomValue = random.NextDouble() * totalWeight;
double cumulativeWeight = 0.0;
foreach (EffectGroupWithWeight weightedGroup in weightedEffectGroups)
{
cumulativeWeight += weightedGroup.Weight;
if (randomValue <= cumulativeWeight)
{
return weightedGroup.EffectGroupId;
}
}
return weightedEffectGroups.Last().EffectGroupId;
}
private static readonly Random _random = new Random();
/// <summary>
/// Selects an option from an effect group based on option_ratio weights and returns a state_effect_id
/// </summary>
/// <param name="options">List of options in the effect group</param>
/// <returns>Selected state_effect_id</returns>
private int SelectOptionFromGroup(List<EquipmentOptionRecord> options)
{
// Safety check to prevent data corruption
if (options == null || options.Count == 0)
throw new InvalidOperationException("No options available in group - this indicates a data consistency issue");
long totalRatio = options.Sum(x => (long)x.OptionRatio);
long randomValue = _random.NextInt64(0, totalRatio);
long cumulativeRatio = 0;
foreach (EquipmentOptionRecord? option in options)
{
cumulativeRatio += option.OptionRatio;
if (randomValue < cumulativeRatio)
{
// Randomly select from the StateEffectList
if (option.StateEffectList == null || option.StateEffectList.Count == 0)
{
throw new InvalidOperationException($"StateEffectList is null or empty for option {option.Id}");
}
int randomIndex = _random.Next(option.StateEffectList.Count);
return option.StateEffectList[randomIndex].StateEffectId;
}
}
// Fallback: randomly select from the StateEffectList of the last option
EquipmentOptionRecord? lastOption = options.Last();
if (lastOption?.StateEffectList == null || lastOption.StateEffectList.Count == 0)
{
throw new InvalidOperationException($"StateEffectList is null or empty for fallback option {lastOption?.Id}");
}
int fallbackIndex = _random.Next(lastOption.StateEffectList.Count);
return lastOption.StateEffectList[fallbackIndex].StateEffectId;
}
private static (int materialId, int materialCost) GetMaterialInfo(int costId)
{
if (GameData.Instance.costTable.TryGetValue(costId, out CostRecord? costRecord) &&
costRecord?.Costs != null &&
costRecord.Costs.Count > 0)
{
return (costRecord.Costs[0].ItemId, costRecord.Costs[0].ItemValue);
}
return (7080001, 1); // Default material ID and cost
}
}
}

View File

@@ -0,0 +1,286 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/equipment/upgradeoption")]
public class UpgradeOption : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqAwakeningUpgradeOption req = await ReadData<ReqAwakeningUpgradeOption>();
User user = GetUser();
ResAwakeningUpgradeOption response = new ResAwakeningUpgradeOption();
// Validate input parameters
if (req.Isn <= 0)
{
Logging.WriteLine($"Invalid ISN: {req.Isn}", LogType.Warning);
await WriteDataAsync(response);
return;
}
// Find the equipment awakening data (prefer old data over new data)
EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn && !x.IsNewData);
if (awakening == null)
{
await WriteDataAsync(response);
return;
}
NetEquipmentAwakeningOption newOption = new NetEquipmentAwakeningOption();
(int optionId, bool isLocked, bool isDisposableLocked)[] slotLockInfo = new (int optionId, bool isLocked, bool isDisposableLocked)[3];
int lockedOptionCount = 0;
for (int i = 1; i <= 3; i++)
{
int currentOptionId = GetOptionIdForSlot(awakening.Option, i);
bool isLocked = IsOptionLocked(awakening.Option, i);
bool isDisposableLocked = IsOptionDisposableLocked(awakening.Option, i);
slotLockInfo[i - 1] = (currentOptionId, isLocked, isDisposableLocked);
if (isLocked || isDisposableLocked)
lockedOptionCount++;
}
// Get cost ID for upgrade based on locked option count
int costId = GetUpgradeCostId(lockedOptionCount);
// Query actual material ID and cost from CostTable.json
(int materialId, int materialCost) = GetMaterialInfo(costId);
ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId);
if (material == null || material.Count < materialCost)
{
await WriteDataAsync(response);
return;
}
if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items))
{
await WriteDataAsync(response);
return;
}
// Process each option slot (1, 2, 3)
ProcessOptionSlots(awakening, newOption, slotLockInfo);
// Create a new awakening entry with the same ISN to preserve the old data
EquipmentAwakeningData newAwakening = new EquipmentAwakeningData()
{
Isn = awakening.Isn,
Option = new NetEquipmentAwakeningOption()
{
Option1Id = newOption.Option1Id,
Option1Lock = newOption.Option1Lock,
IsOption1DisposableLock = newOption.IsOption1DisposableLock,
Option2Id = newOption.Option2Id,
Option2Lock = newOption.Option2Lock,
IsOption2DisposableLock = newOption.IsOption2DisposableLock,
Option3Id = newOption.Option3Id,
Option3Lock = newOption.Option3Lock,
IsOption3DisposableLock = newOption.IsOption3DisposableLock
},
IsNewData = true // newAwakening
};
user.EquipmentAwakenings.Add(newAwakening);
response.ResetOption = new NetEquipmentAwakeningOption()
{
Option1Id = newOption.Option1Id,
Option1Lock = newOption.Option1Lock,
IsOption1DisposableLock = newOption.IsOption1DisposableLock,
Option2Id = newOption.Option2Id,
Option2Lock = newOption.Option2Lock,
IsOption2DisposableLock = newOption.IsOption2DisposableLock,
Option3Id = newOption.Option3Id,
Option3Lock = newOption.Option3Lock,
IsOption3DisposableLock = newOption.IsOption3DisposableLock
};
JsonDb.Save();
await WriteDataAsync(response);
}
private void ProcessOptionSlots(EquipmentAwakeningData awakening, NetEquipmentAwakeningOption newOption, (int optionId, bool isLocked, bool isDisposableLocked)[] slotLockInfo)
{
for (int i = 1; i <= 3; i++)
{
(int currentOptionId, bool isLocked, bool isDisposableLocked) = slotLockInfo[i - 1];
if (isLocked && !isDisposableLocked)
{
SetOptionForSlot(newOption, i, currentOptionId, isLocked, isDisposableLocked);
continue;
}
if (isDisposableLocked)
{
SetOptionForSlot(newOption, i, currentOptionId, false, false);
UnlockDisposableOption(awakening.Option, i);
continue;
}
if (currentOptionId == 0)
{
SetOptionForSlot(newOption, i, 0, false, false);
continue;
}
int newOptionId = GenerateNewOptionId(currentOptionId);
SetOptionForSlot(newOption, i, newOptionId, false, false);
}
}
private void UnlockDisposableOption(NetEquipmentAwakeningOption option, int slot)
{
SetOptionForSlot(option, slot, GetOptionIdForSlot(option, slot), false, false);
}
private int GetOptionIdForSlot(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.Option1Id,
2 => option.Option2Id,
3 => option.Option3Id,
_ => 0
};
}
private bool IsOptionLocked(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.Option1Lock,
2 => option.Option2Lock,
3 => option.Option3Lock,
_ => false
};
}
private bool IsOptionDisposableLocked(NetEquipmentAwakeningOption option, int slot)
{
return slot switch
{
1 => option.IsOption1DisposableLock,
2 => option.IsOption2DisposableLock,
3 => option.IsOption3DisposableLock,
_ => false
};
}
private void SetOptionForSlot(NetEquipmentAwakeningOption option, int slot, int optionId, bool locked, bool disposableLocked)
{
switch (slot)
{
case 1:
option.Option1Id = optionId;
option.Option1Lock = locked;
option.IsOption1DisposableLock = disposableLocked;
break;
case 2:
option.Option2Id = optionId;
option.Option2Lock = locked;
option.IsOption2DisposableLock = disposableLocked;
break;
case 3:
option.Option3Id = optionId;
option.Option3Lock = locked;
option.IsOption3DisposableLock = disposableLocked;
break;
}
}
private int GenerateNewOptionId(int currentStateEffectId)
{
EquipmentOptionRecord? currentOption = GameData.Instance.EquipmentOptionTable.Values
.FirstOrDefault(option => option.StateEffectList != null && option.StateEffectList.Any(se => se.StateEffectId == currentStateEffectId));
if (currentOption == null|| currentOption.EquipmentOptionGroupId != 100000)
{
throw new InvalidOperationException($"Current state_effect_id {currentStateEffectId} not found in any EquipmentOption");
}
int stateEffectGroupId = currentOption.StateEffectGroupId;
List<EquipmentOptionRecord> optionsInGroup = GameData.Instance.EquipmentOptionTable.Values
.Where(x => x.EquipmentOptionGroupId == 100000 && x.StateEffectGroupId == stateEffectGroupId)
.ToList();
if (optionsInGroup.Count == 0)
{
throw new InvalidOperationException($"No awakening options found with state_effect_group_id {stateEffectGroupId}");
}
return SelectOptionFromGroup(optionsInGroup);
}
private static readonly Random _random = new Random();
private int SelectOptionFromGroup(List<EquipmentOptionRecord> options)
{
if (options == null || options.Count == 0)
throw new InvalidOperationException("No options available in group - this indicates a data consistency issue");
long totalRatio = options.Sum(x => (long)x.OptionRatio);
long randomValue = _random.NextInt64(0, totalRatio);
long cumulativeRatio = 0;
foreach (EquipmentOptionRecord option in options)
{
cumulativeRatio += option.OptionRatio;
if (randomValue < cumulativeRatio)
{
// Randomly select from the StateEffectList
if (option.StateEffectList == null || option.StateEffectList.Count == 0)
{
throw new InvalidOperationException($"StateEffectList is null or empty for option {option.Id}");
}
int randomIndex = _random.Next(option.StateEffectList.Count);
return option.StateEffectList[randomIndex].StateEffectId;
}
}
// Fallback: randomly select from the StateEffectList of the last option
EquipmentOptionRecord? lastOption = options.Last();
if (lastOption?.StateEffectList == null || lastOption.StateEffectList.Count == 0)
{
throw new InvalidOperationException($"StateEffectList is null or empty for fallback option {lastOption?.Id}");
}
int fallbackIndex = _random.Next(lastOption.StateEffectList.Count);
return lastOption.StateEffectList[fallbackIndex].StateEffectId;
}
private int GetUpgradeCostId(int lockedOptionCount)
{
// For upgrade operation, use cost_group_id 200
EquipmentOptionCostRecord? costRecord = GameData.Instance.EquipmentOptionCostTable.Values
.FirstOrDefault(x => x.CostGroupId == 200 && x.CostLevel == lockedOptionCount);
return costRecord?.CostId ?? 102001;
}
private static (int materialId, int materialCost) GetMaterialInfo(int costId)
{
if (GameData.Instance.costTable.TryGetValue(costId, out CostRecord? costRecord) &&
costRecord?.Costs != null &&
costRecord.Costs.Count > 0)
{
return (costRecord.Costs[0].ItemId, costRecord.Costs[0].ItemValue);
}
return (7080001, 1); // Default material ID and cost
}
}
}

View File

@@ -0,0 +1,142 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Inventory
{
[PacketPath("/inventory/equipment/lockoption/disposable")]
public class Disposable : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqAwakeningDisposableLockOption req = await ReadData<ReqAwakeningDisposableLockOption>();
User user = GetUser();
ResAwakeningDisposableLockOption response = new ResAwakeningDisposableLockOption();
// Find the equipment awakening data
EquipmentAwakeningData? awakening = user.EquipmentAwakenings.FirstOrDefault(x => x.Isn == req.Isn);
if (awakening == null)
{
await WriteDataAsync(response);
return;
}
int slot = 0;
if (awakening.Option.Option1Id == req.OptionId)
slot = 1;
else if (awakening.Option.Option2Id == req.OptionId)
slot = 2;
else if (awakening.Option.Option3Id == req.OptionId)
slot = 3;
(int materialId, int materialCost) = GetMaterialInfoForAwakening(awakening.Option);
ItemData? material = user.Items.FirstOrDefault(x => x.ItemType == materialId);
if (material == null || material.Count < materialCost)
{
await WriteDataAsync(response);
return;
}
switch (slot)
{
case 1:
if (req.IsLocked)
{
awakening.Option.Option1Lock = true;
awakening.Option.IsOption1DisposableLock = true;
}
else
{
awakening.Option.Option1Lock = false;
awakening.Option.IsOption1DisposableLock = false;
}
break;
case 2:
if (req.IsLocked)
{
awakening.Option.Option2Lock = true;
awakening.Option.IsOption2DisposableLock = true;
}
else
{
awakening.Option.Option2Lock = false;
awakening.Option.IsOption2DisposableLock = false;
}
break;
case 3:
if (req.IsLocked)
{
awakening.Option.Option3Lock = true;
awakening.Option.IsOption3DisposableLock = true;
}
else
{
awakening.Option.Option3Lock = false;
awakening.Option.IsOption3DisposableLock = false;
}
break;
}
if (req.IsLocked)
{
if (!EquipmentUtils.DeductMaterials(material, materialCost, user, response.Items))
{
await WriteDataAsync(response);
return;
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
private static int CalculateMaterialCost(NetEquipmentAwakeningOption option)
{
int lockedOptionCount = 0;
int disposableLockOptionCount = 0;
if (option.Option1Id != 0 && option.Option1Lock && !option.IsOption1DisposableLock)
lockedOptionCount++;
if (option.Option2Id != 0 && option.Option2Lock && !option.IsOption2DisposableLock)
lockedOptionCount++;
if (option.Option3Id != 0 && option.Option3Lock && !option.IsOption3DisposableLock)
lockedOptionCount++;
if (option.Option1Id != 0 && option.Option1Lock && option.IsOption1DisposableLock)
disposableLockOptionCount++;
if (option.Option2Id != 0 && option.Option2Lock && option.IsOption2DisposableLock)
disposableLockOptionCount++;
if (option.Option3Id != 0 && option.Option3Lock && option.IsOption3DisposableLock)
disposableLockOptionCount++;
return GetDisposableFixCostIdByLevel(lockedOptionCount,disposableLockOptionCount);
}
private static int GetDisposableFixCostIdByLevel(int lockedOptionCount,int disposableLockOptionCount)
{
EquipmentOptionCostRecord? costRecord = GameData.Instance.EquipmentOptionCostTable.Values
.FirstOrDefault(x => x.CostGroupId == 100 && x.CostLevel == lockedOptionCount && x.DisposableFixCostLevel == disposableLockOptionCount);
return costRecord?.DisposableFixCostId ?? 101004;
}
private static (int materialId, int materialCost) GetMaterialInfoForAwakening(NetEquipmentAwakeningOption option)
{
int costId = CalculateMaterialCost(option);
return GetMaterialInfo(costId);
}
private static (int materialId, int materialCost) GetMaterialInfo(int costId)
{
if (GameData.Instance.costTable.TryGetValue(costId, out CostRecord? costRecord) &&
costRecord?.Costs != null &&
costRecord.Costs.Count > 0)
{
return (costRecord.Costs[0].ItemId, costRecord.Costs[0].ItemValue);
}
return (7080002, 20); // Default material ID and cost
}
}
}

View File

@@ -3,7 +3,7 @@ using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Jukebox
{
[PacketPath("/jukebox/set/tableId")]
[PacketPath("/jukebox/set/tableid")]
public class SetTableId : LobbyMsgHandler
{
protected override async Task HandleAsync()

View File

@@ -116,8 +116,11 @@ namespace EpinelPS.LobbyServer
msg2.MergeFrom(Contents);
Logging.WriteLine("Reading " + msg2.GetType().Name, LogType.Debug);
PrintMessage(msg2);
Logging.WriteLine("", LogType.Debug);
if (msg2.GetType().Name != "ReqSyncBadge")
{
PrintMessage(msg2);
Logging.WriteLine("", LogType.Debug);
}
return msg2;
}
@@ -130,8 +133,11 @@ namespace EpinelPS.LobbyServer
PacketDecryptResponse bin = await PacketDecryption.DecryptOrReturnContentAsync(ctx);
msg.MergeFrom(bin.Contents);
PrintMessage(msg);
Logging.WriteLine("", LogType.Debug);
if (msg.GetType().Name != "ReqSyncBadge")
{
PrintMessage(msg);
Logging.WriteLine("", LogType.Debug);
}
UserId = bin.UserId;
UsedAuthToken = bin.UsedAuthToken;

View File

@@ -110,6 +110,10 @@ namespace EpinelPS.LobbyServer.LobbyUser
}
response.LastClearedNormalMainStageId = user.LastNormalStageCleared;
response.LastClearedStoryStageId = user.LastStoryStageCleared;
response.LastClearedHardMainStageId = user.LastHardStageCleared;
response.LastClearedMod = user.LastClearedDifficulty;
response.TimeRewardBuffs.AddRange(NetUtils.GetOutpostTimeReward(user));
response.OwnedLobbyDecoBackgroundIdList.AddRange(user.LobbyDecoBackgroundList);

View File

@@ -1,4 +1,5 @@
using EpinelPS.Utils;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.LobbyUser
{
@@ -7,28 +8,69 @@ namespace EpinelPS.LobbyServer.LobbyUser
{
protected override async Task HandleAsync()
{
ReqGetContentsOpenData req = await ReadData<ReqGetContentsOpenData>();
await ReadData<ReqGetContentsOpenData>();
User user = GetUser();
// this request returns a list of "special" stages that mark when something is unlocked, ex: the shop or interception
List<int> specialStages = [6003003, 6002008, 6002016, 6005003, 6003021, 6011018, 6007021, 6004018, 6005013, 6003009, 6003012, 6009017, 6016039, 6001004, 6000003, 6000001, 6002001, 6004023, 6005026, 6020050, 6006004, 6006023,6022049];
ResGetContentsOpenData response = new();
foreach (FieldInfoNew field in user.FieldInfoNew.Values)
List<int> stages = [];
foreach (var item in GameData.Instance.ContentsOpenTable)
{
foreach (int stage in field.CompletedStages)
foreach (var condition in item.Value.OpenCondition)
{
if (specialStages.Contains(stage))
response.ClearStageList.Add(stage);
if (condition.OpenConditionType == ContentsOpenCondition.StageClear && !stages.Contains(condition.OpenConditionValue) && user.IsStageCompleted(condition.OpenConditionValue))
{
stages.Add(condition.OpenConditionValue);
}
}
}
response.MaxGachaCount = 10;
response.MaxGachaPremiumCount = 10;
// these stages are not present in contentsopentable but are required to show mission UI and burst sidebar UI in battle view
List<int> specialStages = [6000001, 6000003];
foreach (var item in specialStages)
{
if (!stages.Contains(item) && user.IsStageCompleted(item)) stages.Add(item);
}
response.ClearStageList.AddRange(stages);
response.MaxGachaCount = user.GachaTutorialPlayCount;
response.MaxGachaPremiumCount = user.GachaTutorialPlayCount;
// todo tutorial playcount of gacha
response.TutorialGachaPlayCount = user.GachaTutorialPlayCount;
// ClearSimRoomChapterList: 已通关的章节列表,用于显示超频选项 SimRoomOC
response.ClearSimRoomChapterList.AddRange(GetClearSimRoomChapterList(user));
await WriteDataAsync(response);
}
private static List<int> GetClearSimRoomChapterList(User user)
{
var clearSimRoomChapterList = new List<int>();
try
{
var currentDifficulty = user.ResetableData.SimRoomData?.CurrentDifficulty ?? 0;
var currentChapter = user.ResetableData.SimRoomData?.CurrentChapter ?? 0;
if (currentDifficulty > 0 && currentChapter > 0)
{
var chapters = GameData.Instance.SimulationRoomChapterTable.Values.Where(c => c.DifficultyId <= currentDifficulty).ToList();
foreach (var chapter in chapters)
{
bool isAdd = chapter.DifficultyId < currentDifficulty ||
(chapter.DifficultyId == currentDifficulty && chapter.Chapter <= currentChapter);
if (isAdd) clearSimRoomChapterList.Add(chapter.Id);
}
}
}
catch (Exception e)
{
Logging.Warn($"GetClearSimRoomChapterList error: {e.Message}");
}
return clearSimRoomChapterList;
}
}
}

View File

@@ -30,6 +30,9 @@ namespace EpinelPS.LobbyServer.LobbyUser
response.RepresentationTeam = NetUtils.GetDisplayedTeam(user);
response.LastClearedNormalMainStageId = user.LastNormalStageCleared;
response.LastClearedStoryStageId = user.LastStoryStageCleared;
response.LastClearedHardMainStageId = user.LastHardStageCleared;
response.LastClearedMod = user.LastClearedDifficulty;
// Restore completed tutorials. GroupID is the first 4 digits of the Table ID.
foreach (KeyValuePair<int, ClearedTutorialData> item in user.ClearedTutorialData)

View File

@@ -1,5 +1,4 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.LobbyUser
{
[PacketPath("/user/scenario/exist")]
@@ -13,21 +12,34 @@ namespace EpinelPS.LobbyServer.LobbyUser
ResExistScenario response = new();
User user = GetUser();
foreach (string? item in req.ScenarioGroupIds)
{
foreach (string completed in user.CompletedScenarios)
if (FindScenarioInMainStages(item) || FindScenarioInArchiveStages(item))
{
// story thingy was completed
if (completed == item)
{
response.ExistGroupIds.Add(item);
}
response.ExistGroupIds.Add(item);
}
}
await WriteDataAsync(response);
}
private bool FindScenarioInMainStages(string scenarioGroupId)
{
User user = GetUser();
return user.CompletedScenarios.Contains(scenarioGroupId);
}
private bool FindScenarioInArchiveStages(string scenarioGroupId)
{
User user = GetUser();
foreach (EventData evtData in user.EventInfo.Values)
{
if (evtData.CompletedScenarios.Contains(scenarioGroupId))
{
return true;
}
}
return false;
}
}
}

View File

@@ -7,11 +7,16 @@ namespace EpinelPS.LobbyServer.LobbyUser
{
protected override async Task HandleAsync()
{
ReqGetUserTitleCounterList req = await ReadData<ReqGetUserTitleCounterList>();
await ReadData<ReqGetUserTitleCounterList>();
ResGetUserTitleCounterList r = new();
ResGetUserTitleCounterList response = new();
response.UserTitleCounterList.Add(new ResGetUserTitleCounterList.Types.NetUserTitleCounter { Condition = 23, SubCondition = 1, Count = 10});
response.UserTitleCounterList.Add(new ResGetUserTitleCounterList.Types.NetUserTitleCounter { Condition = 23, SubCondition = 2, Count = 10});
response.UserTitleCounterList.Add(new ResGetUserTitleCounterList.Types.NetUserTitleCounter { Condition = 23, SubCondition = 3, Count = 10});
response.UserTitleCounterList.Add(new ResGetUserTitleCounterList.Types.NetUserTitleCounter { Condition = 23, SubCondition = 4, Count = 10});
response.UserTitleCounterList.Add(new ResGetUserTitleCounterList.Types.NetUserTitleCounter { Condition = 23, SubCondition = 5, Count = 10});
await WriteDataAsync(r);
await WriteDataAsync(response);
}
}
}

View File

@@ -14,7 +14,7 @@ namespace EpinelPS.LobbyServer.Misc
if (user.GachaTutorialPlayCount > 0)
response.Unavailables.Add(3);
// TODO: ValIdate response from real server and pull info from user info
// TODO: Validate response from real server and pull info from user info
await WriteDataAsync(response);
}
}

View File

@@ -17,7 +17,7 @@ namespace EpinelPS.LobbyServer.Misc
};
// Define maintenance window timestamps
Google.Protobuf.WellKnownTypes.Timestamp maintenanceFrom = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow.AddHours(-2)); // Example: 2 hour ago
/*Google.Protobuf.WellKnownTypes.Timestamp maintenanceFrom = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow.AddHours(-2)); // Example: 2 hour ago
Google.Protobuf.WellKnownTypes.Timestamp maintenanceTo = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow.AddHours(-1)); // Example: 1 hour ago
// Add a new maintenance window
@@ -25,7 +25,7 @@ namespace EpinelPS.LobbyServer.Misc
{
From = maintenanceFrom,
To = maintenanceTo
};
};*/
await WriteDataAsync(r);
}

View File

@@ -0,0 +1,30 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Outpost.Recycle
{
[PacketPath("/outpost/RecycleRoom/PersonalResearchLevelUp")]
public class PersonalResearchLevelUp : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqPersonalResearchRecycleLevelUp req = await ReadData<ReqPersonalResearchRecycleLevelUp>();
ResPersonalResearchRecycleLevelUp response = new();
User user = GetUser();
const int personalResearchTid = 1001;
RecycleRoomResearchProgress personalResearchProgress = user.ResearchProgress[personalResearchTid] ?? throw new Exception("PersonalRearch not found.");
personalResearchProgress.Level += req.LevelUpCount;
response.Recycle = new()
{
Tid = personalResearchTid,
Lv = personalResearchProgress.Level,
Exp = personalResearchProgress.Exp
};
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,23 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Pass
{
[PacketPath("/pass/event/buyrank")]
public class BuyEventPassRank : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "passId": 1037, "targetPassRank": 2 }
ReqBuyEventPassRank req = await ReadData<ReqBuyEventPassRank>(); //fields "PassId", "TargetPassRank"
User user = GetUser();
ResBuyEventPassRank response = new(); // fields "PassRank", "PassPoint", "Currencies"
PassHelper.BuyRank(user, req.PassId, req.TargetPassRank, out int PassPoint, out NetUserCurrencyData currencie);
response.PassRank = req.TargetPassRank;
response.PassPoint = PassPoint;
response.Currencies.Add(currencie);
await WriteDataAsync(response);
}
}
}

View File

@@ -7,12 +7,18 @@ namespace EpinelPS.LobbyServer.Pass
{
protected override async Task HandleAsync()
{
// { "passId": 1037, "targetPassRank": 2 }
ReqBuyPassRank req = await ReadData<ReqBuyPassRank>(); //fields "PassId", "TargetPassRank"
User user = GetUser();
ResBuyPassRank response = new(); // fields "PassRank", "PassPoint", "Currencies"
await WriteDataAsync(response);
PassHelper.BuyRank(user, req.PassId, req.TargetPassRank, out int PassPoint, out NetUserCurrencyData currencie);
response.PassRank = req.TargetPassRank;
response.PassPoint = PassPoint;
response.Currencies.Add(currencie);
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,28 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Pass
{
[PacketPath("/pass/event/completemission")]
public class CompleteEventPassMission : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCompleteEventPassMission req = await ReadData<ReqCompleteEventPassMission>(); //fields "PassId", "PassMissionList"
User user = GetUser();
ResCompleteEventPassMission response = new(); // field Reward
NetRewardData reward = new()
{
PassPoint = { }
};
PassHelper.CompletePassMissions(user, ref reward, req.PassId, [.. req.PassMissionList]);
response.Reward = reward;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,28 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Pass
{
[PacketPath("/pass/completemission")]
public class CompletePassMission : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCompletePassMission req = await ReadData<ReqCompletePassMission>(); //fields "PassId", "PassMissionList"
User user = GetUser();
ResCompletePassMission response = new(); // field Reward
NetRewardData reward = new()
{
PassPoint = { }
};
PassHelper.CompletePassMissions(user, ref reward, req.PassId, [.. req.PassMissionList]);
response.Reward = reward;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -1,18 +1,64 @@
using EpinelPS.Utils;
using EpinelPS.Data;
using EpinelPS.LobbyServer.Event;
using EpinelPS.Utils;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Pass
{
[PacketPath("/pass/event/getactive")]
public class GetActiveEventPassData : LobbyMsgHandler
{
//broken game wont boot if not empty not sure how to implement this one
private static readonly ILog log = LogManager.GetLogger(typeof(GetActiveEventPassData));
protected override async Task HandleAsync()
{
ReqGetActiveEventPassData req = await ReadData<ReqGetActiveEventPassData>(); // no fields
User user = GetUser();
ResGetActiveEventPassData response = new(); // fields PassList = NetPassInfo
List<LobbyPrivateBannerRecord> lobbyPrivateBanners = [];//[.. GameData.Instance.LobbyPrivateBannerTable.Values.Where(b => b.PrivateBannerShowDuration <= DateTime.UtcNow && b.EndDate >= DateTime.UtcNow)];
lobbyPrivateBanners = EventHelper.GetLobbyPrivateBannerData(user);
// TODO: PrivateBannerShowDuration
log.Debug($"Active lobby private banners: {JsonConvert.SerializeObject(lobbyPrivateBanners)}");
if (lobbyPrivateBanners.Count <= 0)
{
// No active lobby private banners
Logging.WriteLine("No active lobby private banners found.", LogType.Warning);
await WriteDataAsync(response);
return;
}
List<int> passIds = [];
foreach (var banner in lobbyPrivateBanners)
{
passIds.AddRange(GameData.Instance.eventManagers.Values.Where(em => em.SetField == banner.EventId && em.EventSystemType == EventSystemType.EventPass).Select(em => em.Id));
}
log.Debug($"Active event pass IDs from banners: {JsonConvert.SerializeObject(passIds)}");
if (passIds.Count == 0)
{
Logging.WriteLine("No active event pass IDs found from lobby private banners.", LogType.Warning);
await WriteDataAsync(response);
return;
}
var passManager = GameData.Instance.EventPassManagerTable.Values.Where(p => passIds.Contains(p.EventId)).ToList();
log.Debug($"Active event pass managers: {JsonConvert.SerializeObject(passManager)}");
if (passManager.Count == 0)
{
Logging.WriteLine("No active event pass found.");
await WriteDataAsync(response);
return;
}
foreach (var pm in passManager)
{
NetPassInfo passInfo = PassHelper.GetPassInfo(user, pm.Id, pm.PassPointId);
if (passInfo.PassId != 0 && passInfo.PassRankList.Count > 0)
{
response.PassList.Add(passInfo);
}
}
await WriteDataAsync(response);
}

View File

@@ -1,30 +1,42 @@
using EpinelPS.Utils;
using EpinelPS.Data;
using EpinelPS.Utils;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Pass
{
[PacketPath("/pass/getactive")]
public class GetActivePassData : LobbyMsgHandler
{
private static readonly ILog log = LogManager.GetLogger(typeof(GetActivePassData));
protected override async Task HandleAsync()
{
ReqGetActivePassData req = await ReadData<ReqGetActivePassData>();
User user = GetUser();
ResGetActivePassData response = new()
{
PassExist = true,
Pass = new NetPassInfo { PassId = 1028, PassPoint = 490, PassSkipCount = 15, PremiumActive = true }
PassExist = false,
};
// Adding PassRankList using a loop
for (int rank = 1; rank <= 15; rank++)
var passManager = GameData.Instance.PassManagerTable.Values.FirstOrDefault(p => p.SeasonStartDate <= DateTime.UtcNow && p.SeasonEndDate >= DateTime.UtcNow);
if (passManager != null)
{
response.Pass.PassRankList.Add(new NetPassRankData { PassRank = rank, IsNormalRewarded = true, IsPremiumRewarded = true });
log.Debug($"Found active pass: {JsonConvert.SerializeObject(passManager)}");
NetPassInfo passInfo = PassHelper.GetPassInfo(user, passManager.Id, passManager.PassPointId);
// Simple validation to ensure we have a valid pass
if (passInfo.PassId != 0 && passInfo.PassRankList.Count > 0)
{
response.PassExist = true;
response.Pass = passInfo;
}
}
else
{
Logging.WriteLine("No active pass found.");
}
int[] missionIds = new[] { 4001, 4002, 4003, 4004, 4005, 4006, 4007 };
foreach (int missionId in missionIds) response.Pass.PassMissionList.Add(new NetPassMissionData { PassMissionId = missionId, IsComplete = true });
await WriteDataAsync(response);
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,27 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Pass
{
[PacketPath("/pass/event/obtainreward")]
public class ObtainEventPassReward : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "passId": 1037, "passRank": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ] }
ReqObtainEventPassReward req = await ReadData<ReqObtainEventPassReward>(); //fields "PassId", "PassRank"
User user = GetUser();
ResObtainEventPassReward response = new(); // field Reward
NetRewardData reward = new()
{
PassPoint = { }
};
PassHelper.ObtainPassRewards(user, ref reward, req.PassId, [.. req.PassRank]);
response.Reward = reward;
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,27 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Pass
{
[PacketPath("/pass/event/obtainonereward")]
public class ObtainOneEventPassReward : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "passId": 1037, "passRank": 1, "premiumReward": true }
ReqObtainOneEventPassReward req = await ReadData<ReqObtainOneEventPassReward>(); //fields "PassId", "PassRank"
User user = GetUser();
ResObtainOneEventPassReward response = new(); // field Reward
NetRewardData reward = new()
{
PassPoint = { }
};
PassHelper.ObtainOnePassRewards(user, ref reward, req.PassId, req.PassRank, req.PremiumReward);
response.Reward = reward;
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,27 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Pass
{
[PacketPath("/pass/obtainonereward")]
public class ObtainOnePassReward : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "passId": 1037, "passRank": 1, "premiumReward": true }
ReqObtainOnePassReward req = await ReadData<ReqObtainOnePassReward>(); //fields "PassId", "PassRank"
User user = GetUser();
ResObtainOnePassReward response = new(); // field Reward
NetRewardData reward = new()
{
PassPoint = { }
};
PassHelper.ObtainOnePassRewards(user, ref reward, req.PassId, req.PassRank, req.PremiumReward);
response.Reward = reward;
await WriteDataAsync(response);
}
}
}

View File

@@ -7,12 +7,21 @@ namespace EpinelPS.LobbyServer.Pass
{
protected override async Task HandleAsync()
{
// { "passId": 1037, "passRank": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ] }
ReqObtainPassReward req = await ReadData<ReqObtainPassReward>(); //fields "PassId", "PassRank"
User user = GetUser();
ResObtainPassReward response = new(); // field Reward
await WriteDataAsync(response);
NetRewardData reward = new()
{
PassPoint = { }
};
PassHelper.ObtainPassRewards(user, ref reward, req.PassId, [.. req.PassRank]);
response.Reward = reward;
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,381 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Pass
{
public static class PassHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(PassHelper));
public static NetPassInfo GetPassInfo(User user, int passId, int passPointId)
{
NetPassInfo passInfo = new();
try
{
var userPass = user.UserPassInfo.GetValueOrDefault(passId);
// If user does not have this pass info, create a new one
if (userPass == null || userPass.PassId == 0)
{
userPass = new PassData
{
PassId = passId,
PremiumActive = true,
PassPoint = 0,
PassRankList = [],
PassMissionList = []
};
}
log.Debug($"UserPassInfo before completing missions: {JsonConvert.SerializeObject(userPass)}");
passInfo.PassId = userPass.PassId;
passInfo.PremiumActive = userPass.PremiumActive;
// Populate PassRankList based on SeasonPassTable
var seasonPass = GameData.Instance.SeasonPassTable.Values.Where(sp => sp.PassId == passInfo.PassId).ToList();
if (seasonPass != null && seasonPass.Count > 0)
{
passInfo.PassPoint = userPass.PassPoint;
foreach (var sp in seasonPass)
{
if (userPass.PassRankList.Any(pr => pr.PassRank == sp.PassRank))
{
// Use existing rank data
var existingRank = userPass.PassRankList.First(pr => pr.PassRank == sp.PassRank);
passInfo.PassRankList.Add(new NetPassRankData
{
PassRank = existingRank.PassRank,
IsNormalRewarded = existingRank.IsNormalRewarded,
IsPremiumRewarded = existingRank.IsPremiumRewarded
});
}
else
{
// If the user does not have this rank yet, add it as not rewarded
userPass.PassRankList.Add(new PassRankData { PassRank = sp.PassRank, IsNormalRewarded = false, IsPremiumRewarded = false });
passInfo.PassRankList.Add(new NetPassRankData { PassRank = sp.PassRank, IsNormalRewarded = false, IsPremiumRewarded = false });
}
}
}
// Populate PassMissionList based on PassMissionTable
var passMissions = GameData.Instance.PassMissionTable.Values.Where(pm => pm.PassPointId == passPointId).ToList();
if (passMissions != null && passMissions.Count > 0)
{
foreach (var pm in passMissions)
{
if (userPass.PassMissionList.Any(m => m.PassMissionId == pm.Id))
{
// Use existing mission data
var existingMission = userPass.PassMissionList.First(m => m.PassMissionId == pm.Id);
existingMission.IsComplete = userPass.LastCompleteAt == DateTime.Now.ToString("yyyyMMdd") && existingMission.IsComplete;
passInfo.PassMissionList.Add(new NetPassMissionData
{
PassMissionId = existingMission.PassMissionId,
IsComplete = existingMission.IsComplete
});
}
else
{
// If the user does not have this mission yet, add it as not complete
userPass.PassMissionList.Add(new PassMissionData { PassMissionId = pm.Id, IsComplete = false });
passInfo.PassMissionList.Add(new NetPassMissionData { PassMissionId = pm.Id, IsComplete = false });
}
}
}
// Update user's pass info in database
if (userPass != null && userPass.PassId != 0)
{
if (!user.UserPassInfo.TryAdd(userPass.PassId, userPass))
user.UserPassInfo[userPass.PassId] = userPass;
}
log.Debug($"UserPassInfo after completing missions: {JsonConvert.SerializeObject(userPass)}");
JsonDb.Save();
}
catch (Exception ex)
{
log.Error($"Error getting pass info for user {user.ID}, PassId: {passId}, PassPointId: {passPointId}, error: {ex.Message}");
}
return passInfo;
}
public static void RewardsForUser(User user, ref NetRewardData reward, int rewardId)
{
try
{
var rewardData = GameData.Instance.RewardDataRecords.GetValueOrDefault(rewardId);
if (rewardData == null)
{
log.Warn($"No reward data found for RewardId: {rewardId}");
return;
}
if (rewardData.UserExp != 0)
{
int newXp = rewardData.UserExp + user.userPointData.ExperiencePoint;
int newLevelExp = GameData.Instance.GetUserMinXpForLevel(user.userPointData.UserLevel);
int newLevel = user.userPointData.UserLevel;
if (newLevelExp == -1)
{
log.Warn("Unknown user level value for xp " + newXp);
}
int newGems = 0;
while (newXp >= newLevelExp)
{
newLevel++;
newGems += 30;
newXp -= newLevelExp;
if (user.Currency.ContainsKey(CurrencyType.FreeCash))
user.Currency[CurrencyType.FreeCash] += 30;
else
user.Currency.Add(CurrencyType.FreeCash, 30);
newLevelExp = GameData.Instance.GetUserMinXpForLevel(newLevel);
}
// TODO: what is the difference between IncreaseExp and GainExp
// NOTE: Current Exp/Lv refers to after XP was added.
reward.UserExp = new NetIncreaseExpData()
{
BeforeExp = user.userPointData.ExperiencePoint,
BeforeLv = user.userPointData.UserLevel,
// IncreaseExp = rewardData.UserExp,
CurrentExp = newXp,
CurrentLv = newLevel,
GainExp = rewardData.UserExp,
};
user.userPointData.ExperiencePoint = newXp;
user.userPointData.UserLevel = newLevel;
}
foreach (var item in rewardData.Rewards)
{
if (item.RewardType != RewardType.None)
{
RewardUtils.AddSingleObject(user, ref reward, item.RewardId, item.RewardType, item.RewardValue);
}
}
}
catch (Exception ex)
{
log.Error($"Error processing rewards for user {user.ID} with RewardId {rewardId}, error: {ex.Message}");
}
}
public static void UpdateUserPassInfoRank(User user, int passId, List<int> rankList, bool IsNormalRewarded, bool IsPremiumRewarded, int key = 0)
{
try
{
if (user.UserPassInfo.TryGetValue(passId, out PassData? passData))
{
foreach (var rank in rankList)
{
if (passData.PassRankList.Any(pr => pr.PassRank == rank))
{
var existingRank = passData.PassRankList.First(pr => pr.PassRank == rank);
if (key == 0 || key == 1) existingRank.IsNormalRewarded = IsNormalRewarded;
if (key == 0 || key == 2) existingRank.IsPremiumRewarded = IsPremiumRewarded;
}
else
{
// If the user does not have this rank yet, add it
passData.PassRankList.Add(new PassRankData
{
PassRank = rank,
IsNormalRewarded = IsNormalRewarded,
IsPremiumRewarded = IsPremiumRewarded
});
}
log.Debug($"Updated pass rank info for user {user.ID} with PassId: {passId}, Ranks: {string.Join(", ", rankList)} userPassInfo: {JsonConvert.SerializeObject(passData)}");
}
JsonDb.Save();
}
else
{
Logging.WriteLine($"No pass data found for user {user.ID} with PassId: {passId}");
}
}
catch (Exception ex)
{
Logging.WriteLine($"Error updating pass rank info for user {user.ID}, PassId: {passId}, error: {ex.Message}");
}
}
public static void ObtainPassRewards(User user, ref NetRewardData reward, int passId, List<int> rankList)
{
try
{
var seasons = GameData.Instance.SeasonPassTable.Values.Where(sp => sp.PassId == passId && rankList.Contains(sp.PassRank)).ToList();
if (seasons.Count == 0)
{
Logging.WriteLine($"No such pass id: {passId} or ranks: {string.Join(", ", rankList)}");
}
else
{
foreach (var season in seasons)
{
bool isFreeRewarded = false;
bool isPremiumRewarded = false;
// check if the user has already claimed these rewards
if (user.UserPassInfo.TryGetValue(passId, out PassData? passData))
{
var existingRank = passData.PassRankList.FirstOrDefault(pr => pr.PassRank == season.PassRank);
if (existingRank != null)
{
isFreeRewarded = existingRank.IsNormalRewarded;
isPremiumRewarded = existingRank.IsPremiumRewarded;
}
}
// give rewards if not already claimed
if (season.FreeReward != 0 && !isFreeRewarded)
RewardsForUser(user, ref reward, season.FreeReward);
if (season.PremiumReward1 != 0 && !isPremiumRewarded)
RewardsForUser(user, ref reward, season.PremiumReward1);
if (season.PremiumReward2 != 0 && !isPremiumRewarded)
RewardsForUser(user, ref reward, season.PremiumReward2);
}
// update user pass info to mark these ranks as rewarded
// No abnormal judgment was made here,
UpdateUserPassInfoRank(user, passId, rankList, true, true);
}
}
catch (Exception ex)
{
Logging.WriteLine($"Error obtaining pass rewards for user {user.ID}, PassId: {passId}, Ranks: {string.Join(", ", rankList)}, error: {ex.Message}");
}
}
public static void ObtainOnePassRewards(User user, ref NetRewardData reward, int passId, int rank, bool premiumReward)
{
try
{
SeasonPassRecord? season = GameData.Instance.SeasonPassTable.Values.Where(sp => sp.PassId == passId && sp.PassRank == rank).FirstOrDefault();
if (season == null || season.PassId == 0)
{
Logging.WriteLine($"No such pass id: {passId} or rank: {rank}");
}
else
{
int key = premiumReward ? 2 : 1;
if (!premiumReward && season.FreeReward != 0)
RewardsForUser(user, ref reward, season.FreeReward);
if (premiumReward && season.PremiumReward1 != 0)
RewardsForUser(user, ref reward, season.PremiumReward1);
if (premiumReward && season.PremiumReward2 != 0)
RewardsForUser(user, ref reward, season.PremiumReward2);
// update user pass info to mark these ranks as rewarded
// No abnormal judgment was made here,
UpdateUserPassInfoRank(user, passId, [season.PassRank], !premiumReward, premiumReward, key);
}
}
catch (Exception ex)
{
Logging.WriteLine($"Error obtaining pass rewards for user {user.ID}, PassId: {passId}, Rank: {rank}, error: {ex.Message}");
}
}
public static void BuyRank(User user, int passId, int targetPassRank, out int passPoint, out NetUserCurrencyData currencie)
{
int rankPrice = 200; // each rank costs 200 currency units
passPoint = 0;
currencie = new();
if (user.Currency.TryGetValue(CurrencyType.ChargeCash, out long value) && value < rankPrice)
{
Logging.WriteLine($"User {user.ID} does not have enough ChargeCash to buy rank. Required: {rankPrice}, Available: {value}");
currencie = new() { Type = (int)CurrencyType.ChargeCash, Value = value };
return;
}
SeasonPassRecord? season = GameData.Instance.SeasonPassTable.Values.Where(sp => sp.PassId == passId && sp.PassRank == targetPassRank).FirstOrDefault();
if (season == null)
{
Logging.WriteLine($"No such pass id: {passId} or rank: {targetPassRank}");
return;
}
user.Currency[CurrencyType.ChargeCash] -= rankPrice;
passPoint = season.ConditionValue;
currencie = new() { Type = (int)CurrencyType.ChargeCash, Value = user.Currency[CurrencyType.ChargeCash] };
if (user.UserPassInfo.TryGetValue(passId, out PassData? passData))
{
passData.PassPoint = passPoint;
}
else
{
passData = new PassData
{
PassId = passId,
PremiumActive = true,
PassPoint = season.ConditionValue,
PassRankList = [],
PassMissionList = []
};
user.UserPassInfo.Add(passId, passData);
}
JsonDb.Save();
}
public static void CompletePassMissions(User user, ref NetRewardData reward, int passId, List<int> missionIds)
{
if (!user.UserPassInfo.TryGetValue(passId, out var passData))
{
passData = new();
user.UserPassInfo.Add(passId, passData);
}
log.Debug($"UserPassInfo before completing missions: {JsonConvert.SerializeObject(passData)}");
int completedPoints = 0;
var completedMissions = GameData.Instance.PassMissionTable.Values
.Where(pm => missionIds.Contains(pm.Id)).ToList();
if (completedMissions.Count == 0)
{
log.Warn($"User {user.ID} has no valid pass missions to complete for pass {passId}");
return;
}
foreach (var mission in completedMissions)
{
var existingMission = passData.PassMissionList.FirstOrDefault(m => m.PassMissionId == mission.Id);
if (existingMission != null && existingMission.IsComplete)
{
log.Warn($"User {user.ID} has already completed pass mission {mission.Id} for pass {passId}");
continue;
}
var rewardEntry = GameData.Instance.GetRewardTableEntry(mission.RewardId);
if (rewardEntry == null || rewardEntry.Rewards.Count == 0)
{
log.Warn($"Unable to find reward entry {mission.RewardId} for pass mission {mission.Id}");
continue;
}
foreach (var rewardItem in rewardEntry.Rewards)
{
if (rewardItem.RewardType == RewardType.PassPoint)
{
completedPoints += rewardItem.RewardValue;
existingMission.IsComplete = true;
passData.LastCompleteAt = DateTime.Now.ToString("yyyyMMdd");
}
else
{
log.Warn($"Unsupported reward type {rewardItem.RewardType} in pass mission {mission.Id}");
}
}
}
user.AddTrigger(Trigger.PointRewardEvent, completedPoints, passId);
passData.PassPoint += completedPoints;
log.Debug($"UserPassInfo after completing missions: {JsonConvert.SerializeObject(passData)}");
reward.PassPoint = new()
{
Value = completedPoints,
FinalValue = passData.PassPoint
};
JsonDb.Save(); // Save user data after updating pass info
}
}
}

View File

@@ -11,7 +11,7 @@ namespace EpinelPS.LobbyServer.Shop.InApp
ResGetCustomPackageSetupData response = new();
// TODO: ValIdate response from real server and pull info from user info
// TODO: Validate response from real server and pull info from user info
await WriteDataAsync(response);
}
}

View File

@@ -11,7 +11,7 @@ namespace EpinelPS.LobbyServer.Shop.PackageShop
ResGetCampaignPackage response = new();
// TODO: ValIdate response from real server and pull info from user info
// TODO: Validate response from real server and pull info from user info
await WriteDataAsync(response);
}
}

View File

@@ -19,6 +19,8 @@ namespace EpinelPS.LobbyServer.Sidestory
response.SideStoryStageDataList.Add(new NetSideStoryStageData() { SideStoryStageId = item, ClearedAt = Timestamp.FromDateTime(DateTime.UtcNow) });
}
response.ViewedSideStoryIds.AddRange(user.ViewedSideStoryStages);
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,28 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Sidestory
{
[PacketPath("/sidestory/view/set")]
public class SetViewed : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSetViewSideStory req = await ReadData<ReqSetViewSideStory>();
User user = GetUser();
ResSetViewSideStory response = new();
foreach (var id in req.ViewedSideStoryIds)
{
if (!user.ViewedSideStoryStages.Contains(id))
{
user.ViewedSideStoryStages.Add(id);
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,61 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using log4net;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/clearbattle")]
public class ClearBattle : LobbyMsgHandler
{
private static readonly ILog log = LogManager.GetLogger(typeof(ClearBattle));
protected override async Task HandleAsync()
{
// {"location":{"chapter":3,"stage":3,"order":2},"event":111011143,"teamNumber":1,"antiCheatAdditionalInfo":{"clientLocalTime":"638993283799771900"}}
ReqClearSimRoomBattle req = await ReadData<ReqClearSimRoomBattle>();
User user = GetUser();
ResClearSimRoomBattle response = new()
{
Result = SimRoomResult.Success
};
// OverclockOptionChangedHps
// Teams
try
{
var team = SimRoomHelper.GetTeamData(user, req.TeamNumber, [.. req.RemainingHps]);
if (team is not null) response.Teams.Add(team);
}
catch (Exception e)
{
log.Error($"ClearBattle Response Team Exception :{e.Message}");
}
SimRoomHelper.UpdateUserRemainingHps(user, [.. req.RemainingHps], req.TeamNumber);
if (req.BattleResult == 1)
{
// BuffOptions
try
{
var buffOptions = SimRoomHelper.GetBuffOptions(user, req.Location);
if (buffOptions is not null && buffOptions.Count > 0)
{
response.BuffOptions.AddRange(buffOptions);
}
}
catch (Exception e)
{
log.Error($"ClearBattle Response BuffOptions Exception :{e.Message}");
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

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