新增功能:实现包含简易模式特性的模拟房间功能 (#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.
This commit is contained in:
qmengz
2025-11-26 23:54:04 +08:00
committed by GitHub
parent 93470b21a4
commit 5dc9945100
23 changed files with 1867 additions and 61 deletions

View File

@@ -291,6 +291,24 @@ namespace EpinelPS.Data
[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 = [];
static async Task<GameData> BuildAsync()
{
await Load();

View File

@@ -0,0 +1,155 @@
using MemoryPack;
namespace EpinelPS.Data;
[MemoryPackable]
public partial class SimulationRoomStageLocationRecord
{
public int Id;
public int ScheduleGroupId;
public int ChapterId;
public int StageGroupId;
public int StageEssentialValue;
public int EventSelectionValue;
public SimulationRoomLocation Location;
public int Weight;
}
[MemoryPackable]
public partial class SimulationRoomBattleEventRecord
{
public int Id { get; set; }
public SimulationRoomEvent EventType { get; set; }
public SimulationRoomScalingType ScalingType { get; set; }
public SimulationRoomConditionType DifficultyConditionType { get; set; }
public int DifficultyConditionValue { get; set; }
public SimulationRoomConditionType ChapterConditionType { get; set; }
public int ChapterConditionValue { get; set; }
public int Weight { get; set; }
public bool SpotAutocontrol { get; set; }
public int MonsterStageLv { get; set; }
public int DynamicObjectStageLv { get; set; }
public int StandardBattlePower { get; set; }
public int StageStatIncreaseGroupId { get; set; }
public bool IsUseQuickBattle { get; set; }
public int SpotId { get; set; }
public bool UseOcMode { get; set; }
public int UseSeasonId { get; set; }
public int BattleEventGroup { get; set; }
}
[MemoryPackable]
public partial class SimulationRoomBuffPreviewRecord
{
public int Id { get; set; }
public SimulationRoomEvent EventType { get; set; }
public PreviewType PreviewType { get; set; }
public string? PreviewTarget { get; set; }
public int Weight { get; set; }
public string? DescriptionLocalkey { get; set; }
}
[MemoryPackable]
public partial class SimulationRoomBuffRecord
{
public int Id { get; set; }
public int GroupId { get; set; }
public SimulationRoomBuffMainTarget MainTarget { get; set; }
public List<SimulationRoomBuffSubTarget>? SubTarget { get; set; }
public SimulationRoomBuffGrade Grade { get; set; }
public int Weight { get; set; }
public SimulationRoomBubbleType BubbleType { get; set; }
public string? NameLocalkey { get; set; }
public string? DescriptionLocalkey { get; set; }
public List<string>? ParameterLocalkey { get; set; }
public string? ResourceId { get; set; }
public SimulationRoomBuffFunctionType FunctionType { get; set; }
public List<SimulationRoomBuffValueData>? BuffValue { get; set; }
}
[MemoryPackable]
public partial class SimulationRoomSelectionEventRecord
{
public int Id { get; set; }
public SimulationRoomEvent EventType { get; set; }
public SimulationRoomConditionType ChapterConditionType { get; set; }
public int ChapterConditionValue { get; set; }
public int SelectionGroupId { get; set; }
public int SelectionValue { get; set; }
public string? NameLocalkey { get; set; }
public string? DescriptionLocalkey { get; set; }
public int Weight { get; set; }
}
[MemoryPackable]
public partial class SimulationRoomBuffValueData
{
public int FunctionValueLevel { get; set; }
public int BattlePowerLevel { get; set; }
}
public enum SimulationRoomScalingType
{
DataRef = 0,
None = 1,
}
public enum SimulationRoomConditionType
{
Range = 0, // 0x0
Select = 1, // 0x0
}
public enum PreviewType
{
MainTarget = 0, // 0x0
Bubble = 1, // 0x0
Grade = 2
}
public enum SimulationRoomBuffMainTarget
{
Shoot = 0, // 0x0
Attack = 1, // 0x0
Survive = 2
}
public enum SimulationRoomBuffSubTarget
{
AR = 0,
RL = 1,
SR = 2,
MG = 3,
SG = 4,
SMG = 5,
ELYSION = 6,
MISSILIS = 7,
TETRA = 8,
PILGRIM = 9,
Attacker = 10,
Defender = 11,
Supporter = 12,
ALL = 13
}
public enum SimulationRoomBuffGrade
{
R = 0,
SR = 1,
SSR = 2,
EPIC = 3
}
public enum SimulationRoomBubbleType
{
TypeA = 0,
TypeB = 1,
TypeC = 2,
TypeD = 3
}
public enum SimulationRoomBuffFunctionType
{
Function = 0,
HealAfterBattle = 1
}

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

View File

@@ -0,0 +1,56 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using log4net;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/fastclearbattle")]
public class ClearBattleFast : 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"}}
ReqFastClearSimRoomBattle req = await ReadData<ReqFastClearSimRoomBattle>();
User user = GetUser();
ResFastClearSimRoomBattle response = new()
{
Result = SimRoomResult.Success
};
// OverclockOptionChangedHps
// Teams
try
{
var team = SimRoomHelper.GetTeamData(user, req.TeamNumber, null);
if (team is not null) response.Teams.Add(team);
}
catch (Exception e)
{
log.Error($"ClearBattleFast Response Team Exception :{e.Message}");
}
SimRoomHelper.UpdateUserRemainingHps(user, teamNumber: req.TeamNumber);
// 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($"ClearBattleFast Response BuffOptions Exception: {e.Message}");
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,20 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/enterbattle")]
public class EnterBattle : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
await ReadData<ReqEnterSimRoomBattle>();
ResEnterSimRoomBattle response = new()
{
Result = SimRoomResult.Success
};
await WriteDataAsync(response);
}
}
}

View File

@@ -1,5 +1,6 @@
using Google.Protobuf.WellKnownTypes;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Simroom
{
@@ -8,17 +9,162 @@ namespace EpinelPS.LobbyServer.Simroom
{
protected override async Task HandleAsync()
{
ReqGetSimRoom req = await ReadData<ReqGetSimRoom>();
await ReadData<ReqGetSimRoom>();
User user = GetUser();
// ResGetSimRoom Fields
// SimRoomStatus Status
// int CurrentDifficulty
// long NextRenewAt
// RepeatedField<NetSimRoomChapterInfo> ClearInfos
// RepeatedField<NetSimRoomEvent> Events
// RepeatedField<NetSimRoomCharacterHp> RemainingHps
// RepeatedField<int> Buffs
// RepeatedField<int> LegacyBuffs
// RepeatedField<int> OverclockOptionList
// NetSimRoomOverclockData OverclockData
// Timestamp NextLegacyBuffResetDate
// NetSimRoomSimpleModeBuffSelectionInfo NextSimpleModeBuffSelectionInfo
// NetSimRoomChapterInfo LastPlayedChapter
// bool IsSimpleModeSkipEnabled
var CurrentDifficulty = user.ResetableData.SimRoomData.CurrentDifficulty;
var currentChapter = user.ResetableData.SimRoomData.CurrentChapter;
ResGetSimRoom response = new()
{
OverclockData = new NetSimRoomOverclockData
Status = SimRoomStatus.Ready,
CurrentDifficulty = CurrentDifficulty,
// NextRenewAt: Resets at 2 AM daily
NextRenewAt = DateTimeHelper.GetNextDayAtTime("China Standard Time", 2).Ticks,
// NextLegacyBuffResetDate: Resets at 2 AM every Tuesday
NextLegacyBuffResetDate = DateTimeHelper.GetNextWeekdayAtTime("China Standard Time", DayOfWeek.Tuesday, 2).ToTimestamp(),
IsSimpleModeSkipEnabled = user.ResetableData.SimRoomData.IsSimpleModeSkipEnabled,
};
// LegacyBuffs
response.LegacyBuffs.AddRange(user.ResetableData.SimRoomData.LegacyBuffs);
// OverclockData
response.OverclockData = GetOverclockData(user: user);
// ClearInfos
response.ClearInfos.AddRange(GetClearInfos(user));
// OverclockOptionList
// response.OverclockOptionList.Add([]);
// check if user is in sim room
if (user.ResetableData.SimRoomData.Entered)
{
response.Status = SimRoomStatus.Progress;
response.Events.AddRange(SimRoomHelper.GetSimRoomEvents(user));
// TODO: Get RemainingHps
response.RemainingHps.AddRange(GetCharacterHp(user));
response.LastPlayedChapter = new NetSimRoomChapterInfo()
{
Chapter = currentChapter,
Difficulty = CurrentDifficulty,
};
// Buffs = Buffs + LegacyBuffs
response.Buffs.AddRange(user.ResetableData.SimRoomData.Buffs);
response.Buffs.AddRange(user.ResetableData.SimRoomData.LegacyBuffs);
// response.NextSimpleModeBuffSelectionInfo = new()
// {
// RemainingBuffSelectCount = 8 - response.Buffs.Count,
// BuffOptions = { user.ResetableData.SimRoomData.Buffs }
// };
}
await WriteDataAsync(response);
}
/// <summary>
/// Get clear infos
/// </summary>
/// <param name="user"></param>
/// <returns>List of cleared chapters</returns>
private static List<NetSimRoomChapterInfo> GetClearInfos(User user)
{
List<NetSimRoomChapterInfo> clearInfos = [];
try
{
var receivedRewards = user.ResetableData.SimRoomData.ReceivedRewardChapters;
if (receivedRewards.Count > 0)
{
// Get the last received reward chapter
var lastReceivedReward = receivedRewards.OrderBy(x => x.Difficulty).ThenBy(x => x.Chapter).LastOrDefault();
if (lastReceivedReward is not null)
{
var CurrentDifficulty = lastReceivedReward.Difficulty;
var CurrentChapter = lastReceivedReward.Chapter;
// Get all chapters where difficulty is less than or equal to current difficulty
var ChapterRecords = GameData.Instance.SimulationRoomChapterTable.Values.Where(x => x.DifficultyId <= CurrentDifficulty).ToList();
foreach (var chapterRecord in ChapterRecords)
{
// check if chapter is less than or equal to current chapter
if (chapterRecord.DifficultyId == CurrentDifficulty && chapterRecord.Chapter <= CurrentChapter)
{
clearInfos.Add(new NetSimRoomChapterInfo()
{
Chapter = chapterRecord.Chapter,
Difficulty = chapterRecord.DifficultyId,
});
}
// check if difficulty is less than current difficulty
else if (chapterRecord.DifficultyId < CurrentDifficulty)
{
clearInfos.Add(new NetSimRoomChapterInfo()
{
Chapter = chapterRecord.Chapter,
Difficulty = chapterRecord.DifficultyId,
});
}
}
}
}
}
catch (Exception e)
{
Logging.WriteLine($"Get ClearInfos Exception: {e.Message}", LogType.Error);
}
return clearInfos;
}
public static List<NetSimRoomCharacterHp> GetCharacterHp(User user)
{
List<NetSimRoomCharacterHp> hps = [];
if (user.UserTeams.TryGetValue((int)TeamType.SimulationRoom, out var userTeamData))
{
if (userTeamData.Teams.Count > 0 && userTeamData.Teams[0].Slots.Count > 0)
{
foreach (var slot in userTeamData.Teams[0].Slots)
{
hps.Add(new() { Csn = slot.Value, Hp = 100000 });
}
}
}
return hps;
}
private static NetSimRoomOverclockData GetOverclockData(User user)
{
return new NetSimRoomOverclockData
{
CurrentSeasonData = new NetSimRoomOverclockSeasonData
{
SeasonStartDate = Timestamp.FromDateTimeOffset(DateTime.UtcNow),
SeasonEndDate = Timestamp.FromDateTimeOffset(DateTime.UtcNow.AddDays(7)),
SeasonStartDate = Timestamp.FromDateTimeOffset(DateTime.UtcNow.Date.AddDays(-1)),
SeasonEndDate = Timestamp.FromDateTimeOffset(DateTime.UtcNow.Date.AddDays(7)),
IsSeasonOpen = true,
Season = 1,
SubSeason = 1,
@@ -27,44 +173,28 @@ namespace EpinelPS.LobbyServer.Simroom
CurrentSeasonHighScore = new NetSimRoomOverclockHighScoreData
{
CreatedAt = Timestamp.FromDateTimeOffset(DateTime.UtcNow),
CreatedAt = Timestamp.FromDateTimeOffset(DateTime.UtcNow.Date.AddDays(-1)),
OptionLevel = 1,
Season = 1,
SubSeason = 1,
OptionList = {}
OptionList = { 1 }
},
CurrentSubSeasonHighScore = new NetSimRoomOverclockHighScoreData
{
CreatedAt = Timestamp.FromDateTimeOffset(DateTime.UtcNow),
CreatedAt = Timestamp.FromDateTimeOffset(DateTime.UtcNow.Date.AddDays(-1)),
OptionLevel = 1,
Season = 1,
SubSeason = 1,
OptionList = {}
OptionList = { 1 }
},
LatestOption = new NetSimRoomOverclockOptionSettingData
{
Season = 1,
OptionList = {}
OptionList = { 1 }
}
},
Status = SimRoomStatus.Ready,
CurrentDifficulty = 1,
NextRenewAt = DateTime.UtcNow.AddDays(7).Ticks,
NextLegacyBuffResetDate = Timestamp.FromDateTimeOffset(DateTime.UtcNow.AddDays(7))
};
if (user.ResetableData.SimRoomData.Entered)
{
response.Status = SimRoomStatus.Progress;
response.CurrentDifficulty = user.ResetableData.SimRoomData.CurrentDifficulty;
}
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,42 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/proceedbufffunction")]
public class ProceedBuffFunction : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "location": { "chapter": 3, "stage": 6, "order": 1 }, "event": 22116, "selectionNumber": 2, "selectionGroupElementId": 221162, "buffToDelete": 2030608 }
ReqProceedSimRoomBuffFunction req = await ReadData<ReqProceedSimRoomBuffFunction>();
// ReqProceedSimRoomBuffFunction Field NetSimRoomEventLocationInfo location, int event, int selectionNumber, int selectionGroupElementId, int buffToDelete
User user = GetUser();
// ReqProceedSimRoomBuffFunction Field SimRoomResult Result, RepeatedField<int> AcquiredBuff, RepeatedField<int> DeletedBuff
ResProceedSimRoomBuffFunction response = new()
{
Result = SimRoomResult.Success
};
if (req.BuffToDelete > 0)
{
response.DeletedBuff.Add(req.BuffToDelete);
}
// Update
var location = req.Location;
// Check
var events = user.ResetableData.SimRoomData.Events;
var simRoomEventIndex = events.FindIndex(x => x.Location.Chapter == location.Chapter && x.Location.Stage == location.Stage && x.Location.Order == location.Order);
if (simRoomEventIndex < 0)
{
Logging.Warn("Not Fond UserSimRoomEvent");
await WriteDataAsync(response);
}
SimRoomHelper.UpdateUserSimRoomEvent(user, index: simRoomEventIndex, events: events, selectionNumber: req.SelectionNumber, isDone: true);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,113 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/proceednikkefunction")]
public class ProceedNikkeFunction : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "location": { "chapter": 3, "stage": 6, "order": 1 }, "event": 10040, "selectionNumber": 1, "selectionGroupElementId": 100401 }
ReqProceedSimRoomNikkeFunction req = await ReadData<ReqProceedSimRoomNikkeFunction>();
User user = GetUser();
ResProceedSimRoomNikkeFunction response = new()
{
Result = SimRoomResult.Success
};
var location = req.Location;
// Check
var events = user.ResetableData.SimRoomData.Events;
var simRoomEventIndex = events.FindIndex(x => x.Location.Chapter == location.Chapter && x.Location.Stage == location.Stage && x.Location.Order == location.Order);
if (simRoomEventIndex < 0)
{
Logging.Warn("Not Fond UserSimRoomEvent");
await WriteDataAsync(response);
}
// changedHps
try
{
if (GameData.Instance.SimulationRoomSelectionGroupTable.TryGetValue(req.SelectionGroupElementId, out var selectionGroup))
{
if (selectionGroup.EventFunctionType == SimulationRoomEventFunctionType.Heal)
{
if (selectionGroup.EventFunctionTargetType == SimulationRoomEventfunctionTargetType.All)
{
var changedRemainingHps = UpdateUserRemainingHps(user, selectionGroup.EventFunctionValue, type: SimulationRoomEventFunctionType.Heal);
response.ChangedHps.AddRange(changedRemainingHps.Select(x => new NetSimRoomCharacterHp { Csn = x.Csn, Hp = x.Hp }));
SimRoomHelper.UpdateUserSimRoomEvent(user, index: simRoomEventIndex, events, selectionNumber: req.SelectionNumber, isDone: true);
}
else
{
Logging.Warn($"Not implement EventFunctionTargetType: {selectionGroup.EventFunctionTargetType}");
}
}
else if (selectionGroup.EventFunctionType == SimulationRoomEventFunctionType.Resurrection)
{
if (selectionGroup.EventFunctionTargetType == SimulationRoomEventfunctionTargetType.All)
{
var changedRemainingHps = UpdateUserRemainingHps(user, selectionGroup.EventFunctionValue, type: SimulationRoomEventFunctionType.Resurrection);
response.ChangedHps.AddRange(changedRemainingHps.Select(x => new NetSimRoomCharacterHp { Csn = x.Csn, Hp = x.Hp }));
SimRoomHelper.UpdateUserSimRoomEvent(user, index: simRoomEventIndex, events, selectionNumber: req.SelectionNumber, isDone: true);
}
else
{
Logging.Warn($"Not implement EventFunctionTargetType: {selectionGroup.EventFunctionTargetType}");
}
}
else
{
Logging.Warn($"Not implement EventFunctionType: {selectionGroup.EventFunctionType}");
}
}
else
{
Logging.Warn("Not Fond SimulationRoomSelectionGroup");
}
}
catch (Exception e)
{
Logging.Warn($"ProceedNikkeFunction ChangedHps Exception {e.Message}");
}
// Teams
var team = SimRoomHelper.GetTeamData(user, 1, null);
if (team is not null) response.Teams.Add(team);
JsonDb.Save();
await WriteDataAsync(response);
}
public static List<SimRoomCharacterHp> UpdateUserRemainingHps(User user, int HpValue, SimulationRoomEventFunctionType type = SimulationRoomEventFunctionType.Heal)
{
var remainingHps = user.ResetableData.SimRoomData.RemainingHps;
if (remainingHps is not null && remainingHps.Count > 0)
{
for (int i = 0; i < remainingHps.Count; i++)
{
if (type is SimulationRoomEventFunctionType.Heal && remainingHps[i].Hp >= 0)
{
// Heal
remainingHps[i] = new SimRoomCharacterHp { Csn = remainingHps[i].Csn, Hp = HpValue };
}
else if (type is SimulationRoomEventFunctionType.Resurrection && remainingHps[i].Hp < 0)
{
// Resurrection
remainingHps[i] = new SimRoomCharacterHp { Csn = remainingHps[i].Csn, Hp = HpValue };
}
}
user.ResetableData.SimRoomData.RemainingHps = remainingHps;
return remainingHps;
}
return remainingHps;
}
}
}

View File

@@ -16,7 +16,35 @@ namespace EpinelPS.LobbyServer.Simroom
Result = SimRoomResult.Success,
};
try
{
foreach (var item in req.BuffsToAdd)
{
if (!user.ResetableData.SimRoomData.LegacyBuffs.Contains(item))
user.ResetableData.SimRoomData.LegacyBuffs.Add(item);
}
}
catch (Exception e)
{
Logging.Warn($"QuitSimRoom BuffsToAdd Exception {e.Message}");
}
try
{
foreach (var item in req.BuffsToDelete)
{
user.ResetableData.SimRoomData.LegacyBuffs.Remove(item);
}
}
catch (Exception e)
{
Logging.Warn($"QuitSimRoom BuffsToDelete Exception {e.Message}");
}
user.ResetableData.SimRoomData.Entered = false;
user.ResetableData.SimRoomData.Events = [];
user.ResetableData.SimRoomData.RemainingHps = [];
user.ResetableData.SimRoomData.Buffs = [];
JsonDb.Save();

View File

@@ -0,0 +1,76 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/selectbuff")]
public class SelectBuff : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// {"location":{"chapter":3,"stage":4,"order":3},"event":111011152,"buffToAdd":1030504}
ReqSelectSimRoomBuff req = await ReadData<ReqSelectSimRoomBuff>();
User user = GetUser();
ResSelectSimRoomBuff response = new()
{
Result = SimRoomResult.Success
};
// Update User SimRoomData Buffs
try
{
var buffs = user.ResetableData.SimRoomData.Buffs;
if (req.BuffToDelete > 0)
{
if (buffs.Contains(req.BuffToDelete)) buffs.Remove(req.BuffToDelete);
}
if (req.BuffToAdd > 0)
{
if (!buffs.Contains(req.BuffToDelete)) buffs.Add(req.BuffToAdd);
}
user.ResetableData.SimRoomData.Buffs = buffs;
}
catch (Exception e)
{
Logging.Warn($"Update User SimRoomData Buffs Exception: {e.Message}");
}
// NetRewardData Reward
// NetRewardData RewardByRewardUpEvent
// NetRewardData RewardByOverclock
var location = req.Location;
if (location is not null)
{
var events = user.ResetableData.SimRoomData.Events;
var simRoomEventIndex = events.FindIndex(x => x.Location.Chapter == location.Chapter && x.Location.Stage == location.Stage && x.Location.Order == location.Order);
if (simRoomEventIndex > -1)
{
// Update User SimRoomData Events
SimRoomHelper.UpdateUserSimRoomEvent(user, simRoomEventIndex, events, battleProgress: (int)SimRoomBattleEventProgress.RewardReceived);
// Reward
var sorted = events.OrderBy(x => x.Location.Stage).ThenBy(x => x.Location.Order).ToList(); // Sort by Stage, Order
var last = sorted[^1]; // Get last event
if (last.Location.Chapter == location.Chapter && last.Location.Stage == location.Stage && last.Location.Order == location.Order)
{
var difficulty = user.ResetableData.SimRoomData.CurrentDifficulty;
var reward = SimRoomHelper.SimRoomReceivedReward(user, difficulty, location.Chapter);
if (reward is not null) response.Reward = reward;
}
}
JsonDb.Save();
}
await WriteDataAsync(response);
}
}
}

View File

@@ -16,14 +16,18 @@ namespace EpinelPS.LobbyServer.Simroom
Result = SimRoomResult.Success,
};
user.ResetableData.SimRoomData.Entered = true;
user.ResetableData.SimRoomData.CurrentDifficulty = req.Difficulty;
user.ResetableData.SimRoomData.CurrentChapter = req.StartingChapter;
// TODO: generate buffs
JsonDb.Save();
List<NetSimRoomEvent> events = SimRoomHelper.GetSimRoomEvents(user);
user.ResetableData.SimRoomData.Events = [.. events.Select(SimRoomHelper.NetToM)];
JsonDb.Save();
response.Events.AddRange(events);
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,23 @@
using EpinelPS.Utils;
using EpinelPS.Database;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/selectevent")]
public class SelectEvent : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "location": { "chapter": 3, "stage": 1, "order": 3 }, "event": 111021115 }
ReqSelectSimRoomEvent req = await ReadData<ReqSelectSimRoomEvent>();
// User user = GetUser();
ResSelectSimRoomEvent response = new()
{
Result = SimRoomResult.Success,
};
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,34 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/selectselectionevent")]
public class SelectSelectionEvent : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSelectSimRoomSelectionEvent req = await ReadData<ReqSelectSimRoomSelectionEvent>();
User user = GetUser();
ResSelectSimRoomSelectionEvent response = new()
{
Result = SimRoomResult.Success
};
var location = req.Location;
// Check
var events = user.ResetableData.SimRoomData.Events;
var simRoomEventIndex = events.FindIndex(x => x.Location.Chapter == location.Chapter && x.Location.Stage == location.Stage && x.Location.Order == location.Order);
if (simRoomEventIndex < 0)
{
Logging.Warn("Not Fond UserSimRoomEvent");
await WriteDataAsync(response);
}
SimRoomHelper.UpdateUserSimRoomEvent(user, index: simRoomEventIndex, events: events, selectionNumber: req.SelectionNumber);
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,633 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
using log4net;
using Newtonsoft.Json;
namespace EpinelPS.LobbyServer.Simroom
{
public static class SimRoomHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(SimRoomHelper));
private static readonly Random _rand = new();
/// <summary>
/// Get SimRoomEvent By User or DifficultyId and ChapterId
/// </summary>
/// <param name="user"> User </param>
/// <param name="difficultyId"> DifficultyId </param>
/// <param name="chapterId"> ChapterId </param>
/// <returns>The list of NetSimRoomEvent</returns>
public static List<NetSimRoomEvent> GetSimRoomEvents(User user, int difficultyId = 0, int chapterId = 0)
{
List<NetSimRoomEvent> netSimRooms = [];
int currentDifficulty = user.ResetableData.SimRoomData.CurrentDifficulty;
int currentChapter = user.ResetableData.SimRoomData.CurrentChapter;
if (difficultyId > 0) currentDifficulty = difficultyId;
if (chapterId > 0) currentChapter = chapterId;
var events = user.ResetableData.SimRoomData.Events;
if (events.Count > 1)
{
return [.. events.Select(MToNet)];
}
var simRoomChapter = GameData.Instance.SimulationRoomChapterTable.Values.FirstOrDefault(x => x.Chapter == currentChapter && x.DifficultyId == currentDifficulty);
log.Debug($"Fond SimulationRoomChapter Chapter: {currentChapter}, DifficultyId: {currentDifficulty} Data: {JsonConvert.SerializeObject(simRoomChapter)}");
var buffPreviewRecords = GameData.Instance.SimulationRoomBuffPreviewTable.Values.ToList();
var simRoomBattleEventRecords = GameData.Instance.SimulationRoomBattleEventTable.Values.ToList();
var simRoomSelectionEventRecords = GameData.Instance.SimulationRoomSelectionEventTable.Values.ToList();
var simRoomSelectionEventGroupRecords = GameData.Instance.SimulationRoomSelectionGroupTable.Values.ToList();
if (simRoomChapter is null) return netSimRooms;
for (int i = 1; i <= simRoomChapter.StageValue; i++)
{
if (i == simRoomChapter.StageValue) // BossBattle
{
var battleBuffPreviews = GameData.Instance.SimulationRoomBuffPreviewTable.Values.Where(x => x.EventType is SimulationRoomEvent.BossBattle).ToList();
var randomBuffPreview = GetRandomItems(battleBuffPreviews, 1);
var simRoomBattleEvent = CreateSimRoomBattleEvent(chapter: simRoomChapter, stage: i, order: 1, simRoomBattleEventRecords, randomBuffPreview[0]);
events.Add(NetToM(simRoomBattleEvent));
netSimRooms.Add(simRoomBattleEvent);
}
else if (i == simRoomChapter.StageValue - 1) // Selection
{
// Maintenance Selection
var simRoomSelectionEventRecord = simRoomSelectionEventRecords.OrderBy(x => x.Id).ToList()
.FindLast(x => x.EventType is SimulationRoomEvent.Maintenance);
var simRoomEvent = new NetSimRoomEvent()
{
Location = new NetSimRoomEventLocationInfo { Chapter = simRoomChapter.Chapter, Stage = i, Order = 1 },
};
if (simRoomSelectionEventRecord is not null)
{
simRoomEvent.Selection = new NetSimRoomSelectionEvent
{
Id = simRoomSelectionEventRecord.Id,
SelectedNumber = 1
};
var groupRecordsBySelectionGroupId = simRoomSelectionEventGroupRecords.FindAll(x => x.SelectionGroupId == simRoomSelectionEventRecord.SelectionGroupId);
foreach (var groupRecord in groupRecordsBySelectionGroupId)
{
simRoomEvent.Selection.Group.Add(new NetSimRoomSelectionGroupElement { SelectionNumber = groupRecord.SelectionNumber, Id = groupRecord.Id });
}
}
events.Add(NetToM(simRoomEvent));
netSimRooms.Add(simRoomEvent);
log.Debug($"stage: {i}, NetSimRoomEvent: {JsonConvert.SerializeObject(simRoomEvent)}");
// Random
var RandomSimRoomSelectionEventRecords = simRoomSelectionEventRecords.FindAll(x
=> x.EventType is SimulationRoomEvent.RandomSelection or SimulationRoomEvent.EnhanceBuff);
var RandomSimRoomSelectionEventRecord = GetRandomItems(RandomSimRoomSelectionEventRecords, 1);
var RandomSimRoomEvent = new NetSimRoomEvent
{
Location = new NetSimRoomEventLocationInfo { Chapter = simRoomChapter.Chapter, Stage = i, Order = 2 },
};
if (RandomSimRoomSelectionEventRecord != null && RandomSimRoomSelectionEventRecord.Count >= 1)
{
RandomSimRoomEvent.Selection = new NetSimRoomSelectionEvent
{
Id = RandomSimRoomSelectionEventRecord[0].Id,
SelectedNumber = 2
};
var groupRecordsBySelectionGroupId = simRoomSelectionEventGroupRecords.FindAll(x
=> x.SelectionGroupId == RandomSimRoomSelectionEventRecord[0].SelectionGroupId);
foreach (var groupRecord in groupRecordsBySelectionGroupId)
{
RandomSimRoomEvent.Selection.Group.Add(new NetSimRoomSelectionGroupElement { SelectionNumber = groupRecord.SelectionNumber, Id = groupRecord.Id });
}
}
events.Add(NetToM(RandomSimRoomEvent));
netSimRooms.Add(RandomSimRoomEvent);
log.Debug($"stage: {i}, NetSimRoomEvent: {JsonConvert.SerializeObject(RandomSimRoomEvent)}");
}
else
{
var battleBuffPreviews = buffPreviewRecords.FindAll(x
=> x.EventType is SimulationRoomEvent.NormalBattle or SimulationRoomEvent.EliteBattle);
var randomBuffPreview = GetRandomItems(battleBuffPreviews, 3);
int order = 0;
foreach (var simRoomBuffPreview in randomBuffPreview)
{
order += 1;
var simRoomBattleEvent = CreateSimRoomBattleEvent(chapter: simRoomChapter, stage: i, order: order, simRoomBattleEventRecords, simRoomBuffPreview);
events.Add(NetToM(simRoomBattleEvent));
netSimRooms.Add(simRoomBattleEvent);
}
}
}
// user.AddTrigger()
user.ResetableData.SimRoomData.Events = events;
JsonDb.Save();
return netSimRooms;
}
/// <summary>
/// Get BuffOptions By User And SimRoomEventLocationInfo
/// </summary>
/// <param name="user"> User </param>
/// <param name="location"> NetSimRoomEventLocationInfo </param>
/// <returns>List<int></returns>
public static List<int> GetBuffOptions(User user, NetSimRoomEventLocationInfo location)
{
var events = user.ResetableData.SimRoomData.Events;
var simRoomEventIndex = events.FindIndex(x => x.Location.Chapter == location.Chapter && x.Location.Stage == location.Stage && x.Location.Order == location.Order);
if (simRoomEventIndex > -1)
{
var simRoomEvent = events[simRoomEventIndex];
if (GameData.Instance.SimulationRoomBuffPreviewTable.TryGetValue(simRoomEvent.Battle.BuffPreviewId, out var buffPreview))
{
log.Debug($"SimRoomBuffPreview: {JsonConvert.SerializeObject(buffPreview)}");
List<SimulationRoomBuffRecord> buffRecords = [];
if (buffPreview.PreviewType is PreviewType.Bubble)
{
var bubbleType = GetBubbleType(buffPreview.PreviewTarget);
buffRecords = [.. GameData.Instance.SimulationRoomBuffTable.Values
.Where(x => x.BubbleType == bubbleType
&& x.Grade is SimulationRoomBuffGrade.SR or SimulationRoomBuffGrade.SSR )];
}
else if (buffPreview.PreviewType is PreviewType.MainTarget)
{
var mainTarget = GetMainTarget(buffPreview.PreviewTarget);
buffRecords = [.. GameData.Instance.SimulationRoomBuffTable.Values
.Where(x => x.MainTarget == mainTarget
&& x.Grade is SimulationRoomBuffGrade.SR or SimulationRoomBuffGrade.SSR)];
}
else if (buffPreview.PreviewType is PreviewType.Grade)
{
buffRecords = [.. GameData.Instance.SimulationRoomBuffTable.Values
.Where(x => x.Grade == SimulationRoomBuffGrade.EPIC)];
}
else
{
log.Warn($"Not Implement SimulationRoomBuffPreview.PreviewType: {buffPreview.PreviewType}");
}
if (buffRecords.Count > 0)
{
var selectedBuffs = buffRecords.GetRandomItems(3);
log.Debug($"Selected Buffs: {JsonConvert.SerializeObject(selectedBuffs)}");
// user SimRoomEvent update
UpdateUserSimRoomEvent(user, simRoomEventIndex, events, battleProgress: (int)SimRoomBattleEventProgress.RewardWaiting, BuffOptions: [.. selectedBuffs.Select(x => x.Id)]);
return [.. selectedBuffs.Select(x => x.Id)];
}
else
{
log.Warn($"Not Font SimulationRoomBuff");
}
}
}
else
{
log.Warn($"Not Font User.ResetableData.SimRoomData.Events");
}
return [];
}
/// <summary>
/// Get TeamData By User Or SimRoomCharacterHp
/// </summary>
/// <param name="user"> User </param>
/// <param name="teamNumber"> Int </param>
/// <param name="remainingHps"> List<NetSimRoomCharacterHp> </param>
/// <returns> NetTeamData </returns>
public static NetTeamData GetTeamData(User user, int teamNumber, List<NetSimRoomCharacterHp>? remainingHps)
{
try
{
if (remainingHps is not null && remainingHps.Count > 0)
{
var team = new NetTeamData()
{
TeamNumber = teamNumber
};
int slot = 1;
foreach (var item in remainingHps)
{
team.Slots.Add(new NetTeamSlot() { Slot = slot, Value = item.Csn });
slot += 1;
}
return team;
}
else
{
if (user.UserTeams.TryGetValue((int)TeamType.SimulationRoom, out var teamData))
{
var team = teamData.Teams.FirstOrDefault(t => t.TeamNumber == teamNumber);
if (team is not null) return team;
}
}
}
catch (Exception e)
{
log.Error($"Get User Teams Exception: {e.Message}");
}
// default value is null
return null;
}
/// <summary>
/// Randomly select a specified number of elements from the list.
/// </summary>
public static List<T> GetRandomItems<T>(this List<T>? list, int count)
{
if (list is null || list.Count == 0)
return [];
// If the number of requests exceeds the list length, retrieve all.
count = Math.Min(count, list.Count);
return [.. list.OrderBy(x => _rand.Next()).Take(count)];
}
/// <summary>
/// Create SimRoomBattleEvent
/// </summary>
/// <param name="chapter"> SimulationRoomChapterRecord </param>
/// <param name="stage"></param>
/// <param name="order"></param>
/// <param name="simRoomBattleEventRecords"></param>
/// <param name="randomBuffPreview"></param>
/// <returns>NetSimRoomEvent</returns>
private static NetSimRoomEvent CreateSimRoomBattleEvent(SimulationRoomChapterRecord chapter, int stage, int order,
List<SimulationRoomBattleEventRecord> simRoomBattleEventRecords, SimulationRoomBuffPreviewRecord randomBuffPreview)
{
var simRoomEvent = new NetSimRoomEvent();
var location = new NetSimRoomEventLocationInfo();
var battle = new NetSimRoomBattleEvent();
location.Chapter = chapter.Chapter;
location.Stage = stage;
location.Order = order;
var simRoomBuffPreviewBattleEvents = simRoomBattleEventRecords.FindAll(x
=> x.EventType == randomBuffPreview.EventType && x.DifficultyConditionValue <= chapter.DifficultyId);
log.Debug($"EventType: {randomBuffPreview.EventType}, SimRoomBattleEventRecord Count: {simRoomBuffPreviewBattleEvents.Count}");
var randomBattleEvents = GetRandomItems(simRoomBuffPreviewBattleEvents, 1);
foreach (var battleEvent in randomBattleEvents)
{
battle.Id = battleEvent.Id;
battle.RemainingTargetHealth = 10000;
battle.BuffPreviewId = randomBuffPreview.Id;
}
simRoomEvent.Location = location;
simRoomEvent.Battle = battle;
log.Debug($"stage: {stage}, NetSimRoomEvent: {JsonConvert.SerializeObject(simRoomEvent)}");
return simRoomEvent;
}
public static NetSimRoomEvent MToNet(SimRoomEvent simRoomEvent)
{
var netSimRoomEvent = new NetSimRoomEvent
{
Selected = simRoomEvent.Selected,
};
if (simRoomEvent.Location is not null && simRoomEvent.Location.Chapter > 0)
{
netSimRoomEvent.Location = new NetSimRoomEventLocationInfo
{
Chapter = simRoomEvent.Location.Chapter,
Stage = simRoomEvent.Location.Stage,
Order = simRoomEvent.Location.Order
};
}
if (simRoomEvent.Battle is not null && simRoomEvent.Battle.Id > 0)
{
netSimRoomEvent.Battle = new NetSimRoomBattleEvent
{
Id = simRoomEvent.Battle.Id,
BuffOptions = { simRoomEvent.Battle.BuffOptions },
Progress = (SimRoomBattleEventProgress)simRoomEvent.Battle.Progress,
RemainingTargetHealth = simRoomEvent.Battle.RemainingTargetHealth,
BuffPreviewId = simRoomEvent.Battle.BuffPreviewId,
};
}
if (simRoomEvent.Selection is not null && simRoomEvent.Selection.Id > 0)
{
netSimRoomEvent.Selection = new NetSimRoomSelectionEvent
{
Id = simRoomEvent.Selection.Id,
SelectedNumber = simRoomEvent.Selection.SelectedNumber,
};
simRoomEvent.Selection.Group.ForEach(g =>
{
netSimRoomEvent.Selection.Group.Add(new NetSimRoomSelectionGroupElement
{
SelectionNumber = g.SelectionNumber,
Id = g.Id,
IsDone = g.IsDone,
RandomBuff = g.RandomBuff,
});
});
}
return netSimRoomEvent;
}
public static SimRoomEvent NetToM(NetSimRoomEvent simRoomEvent)
{
var netSimRoomEvent = new SimRoomEvent
{
Selected = simRoomEvent.Selected,
EventCase = (int)simRoomEvent.EventCase,
};
if (simRoomEvent.Location is not null && simRoomEvent.Location.Chapter > 0)
{
netSimRoomEvent.Location = new SimRoomEventLocationInfo
{
Chapter = simRoomEvent.Location.Chapter,
Stage = simRoomEvent.Location.Stage,
Order = simRoomEvent.Location.Order
};
}
if (simRoomEvent.Battle is not null && simRoomEvent.Battle.Id > 0)
{
netSimRoomEvent.Battle = new SimRoomBattleEvent
{
Id = simRoomEvent.Battle.Id,
Progress = (int)simRoomEvent.Battle.Progress,
RemainingTargetHealth = simRoomEvent.Battle.RemainingTargetHealth,
BuffPreviewId = simRoomEvent.Battle.BuffPreviewId,
};
foreach (var item in simRoomEvent.Battle.BuffOptions)
{
netSimRoomEvent.Battle.BuffOptions.Add(item);
}
}
if (simRoomEvent.Selection is not null && simRoomEvent.Selection.Id > 0)
{
netSimRoomEvent.Selection = new SimRoomSelectionEvent
{
Id = simRoomEvent.Selection.Id,
SelectedNumber = simRoomEvent.Selection.SelectedNumber,
};
foreach (var g in simRoomEvent.Selection.Group)
{
netSimRoomEvent.Selection.Group.Add(new SimRoomSelectionGroupElement
{
SelectionNumber = g.SelectionNumber,
Id = g.Id,
IsDone = g.IsDone,
RandomBuff = g.RandomBuff,
});
}
}
return netSimRoomEvent;
}
public static SimulationRoomBubbleType GetBubbleType(string previewTarget)
{
// Type_C
switch (previewTarget)
{
case "Type_A":
return SimulationRoomBubbleType.TypeA;
case "Type_B":
return SimulationRoomBubbleType.TypeB;
case "Type_C":
return SimulationRoomBubbleType.TypeC;
case "Type_D":
return SimulationRoomBubbleType.TypeD;
default:
log.Warn("Unknown preview target: " + previewTarget);
return SimulationRoomBubbleType.TypeA;
}
}
public static SimulationRoomBuffMainTarget GetMainTarget(string previewTarget)
{
// Shoot = 0, Attack = 1, Survive = 2
switch (previewTarget)
{
case "Shoot":
return SimulationRoomBuffMainTarget.Shoot;
case "Attack":
return SimulationRoomBuffMainTarget.Attack;
case "Survive":
return SimulationRoomBuffMainTarget.Survive;
default:
log.Warn("Unknown preview target: " + previewTarget);
return SimulationRoomBuffMainTarget.Attack;
}
}
/// <summary>
/// Update User SimRoomEvent Events
/// </summary>
public static void UpdateUserSimRoomEvent(User user, int index, List<SimRoomEvent> events, int selectionNumber = 0,
bool isDone = false, int battleProgress = 0, List<int>? BuffOptions = null)
{
try
{
var simRoomEvent = events[index];
// user SimRoomEvent update
simRoomEvent.Selected = true;
// Update Selection Group
var groupIndex = simRoomEvent.Selection.Group.FindIndex(x => x.SelectionNumber == selectionNumber);
if (groupIndex > -1 && isDone)
{
var group = simRoomEvent.Selection.Group[groupIndex];
group.IsDone = isDone;
simRoomEvent.Selection.Group[groupIndex] = group;
}
else
{
log.Warn("Not Fond SimRoomSelectionEvent.Group");
}
// Udapte Battle Progress
if (battleProgress > 0) simRoomEvent.Battle.Progress = battleProgress;
// Update BuffOptions
if (BuffOptions is not null && BuffOptions.Count > 0)
{
simRoomEvent.Battle.BuffOptions = BuffOptions;
}
events[index] = simRoomEvent;
user.ResetableData.SimRoomData.Events = events;
log.Debug($"UpdateUserSimRoomEvent After UserSimRoomEventData: {JsonConvert.SerializeObject(user.ResetableData.SimRoomData.Events)}");
}
catch (Exception e)
{
log.Error($"Update UserSimRoomEvent Events Exception: {e.Message}");
}
}
public static List<SimRoomCharacterHp> UpdateUserRemainingHps(User user, List<NetSimRoomCharacterHp>? remainingHps = null, int teamNumber = 1, int HpValue = 10000)
{
var userRemainingHps = user.ResetableData.SimRoomData.RemainingHps;
try
{
// req remainingHps
if (remainingHps is not null && remainingHps.Count > 0)
{
foreach (var characterHp in remainingHps)
{
var userCharacterHpIndex = userRemainingHps.FindIndex(x => x.Csn == characterHp.Csn);
if (userCharacterHpIndex > -1)
{
userRemainingHps[userCharacterHpIndex] = new SimRoomCharacterHp { Csn = characterHp.Csn, Hp = characterHp.Hp };
}
else
{
userRemainingHps.Add(new SimRoomCharacterHp { Csn = characterHp.Csn, Hp = characterHp.Hp });
}
}
}
// get user team
if (user.UserTeams.TryGetValue((int)TeamType.SimulationRoom, out var userTeam))
{
var team = userTeam.Teams.FirstOrDefault(x => x.TeamNumber == teamNumber);
if (team is not null)
{
foreach (var slot in team.Slots)
{
if (userRemainingHps.FindIndex(x => x.Csn == slot.Value) < 0) userRemainingHps.Add(new SimRoomCharacterHp { Csn = slot.Value, Hp = HpValue });
}
}
}
// update user
user.ResetableData.SimRoomData.RemainingHps = userRemainingHps;
return userRemainingHps;
}
catch (Exception e)
{
log.Error($"Update UserRemainingHps Exception: {e.Message}");
}
return userRemainingHps;
}
public static NetRewardData? SimRoomReceivedReward(User user, int difficultyId, int chapterId)
{
// check if reward is received
NetRewardData? reward = null;
if (!IsRewardReceived(user, difficultyId, chapterId))
{
var allChapterRecords = GameData.Instance.SimulationRoomChapterTable.Values;
var chapter = allChapterRecords
.FirstOrDefault(x => x.DifficultyId == difficultyId && x.Chapter == chapterId);
if (chapter is not null && chapter.RewardId > 0)
{
var receivedRewardChapters = user.ResetableData.SimRoomData.ReceivedRewardChapters;
var receivedRewardChapter = receivedRewardChapters
.OrderBy(x => x.Difficulty)
.ThenBy(x => x.Chapter)
.LastOrDefault();
SimulationRoomChapterRecord? receivedRewardChapterRecord = null;
if (receivedRewardChapter is not null)
{
receivedRewardChapterRecord = allChapterRecords
.FirstOrDefault(x => x.DifficultyId == receivedRewardChapter.Difficulty
&& x.Chapter == receivedRewardChapter.Chapter);
}
reward = new NetRewardData();
if (receivedRewardChapterRecord is null)
{
// Claiming the reward for the first time
reward = RewardUtils.RegisterRewardsForUser(user, rewardId: chapter.RewardId);
AddRewardChapter(user, difficultyId, chapterId);
}
// Check if the received reward chapter is lower than the current chapter
else if (receivedRewardChapterRecord.DifficultyId == difficultyId && receivedRewardChapterRecord.Chapter < chapter.Chapter)
{
// Claiming the reward for a higher chapter
reward = CalculateIncrementalReward(user, chapter, receivedRewardChapterRecord);
AddRewardChapter(user, difficultyId, chapterId);
}
// Check if the received reward chapter is lower than the current difficulty
else if (receivedRewardChapterRecord.DifficultyId < difficultyId)
{
// Claiming the reward for a higher difficulty
reward = RewardUtils.RegisterRewardsForUser(user, rewardId: chapter.RewardId);
AddRewardChapter(user, difficultyId, chapterId);
}
}
}
return reward;
}
/// <summary>
/// Check if reward is received
/// </summary>
/// <returns>True if reward is received, otherwise false</returns>
private static bool IsRewardReceived(User user, int difficultyId, int chapterId)
{
var isReceived = user.ResetableData.SimRoomData.ReceivedRewardChapters.Any(x => x.Chapter == chapterId && x.Difficulty == difficultyId);
log.Debug($"IsRewardReceived (diff: {difficultyId}, chapter: {chapterId}): {isReceived}");
return isReceived;
}
/// <summary>
/// Add reward chapter
/// </summary>
private static void AddRewardChapter(User user, int difficulty, int chapter)
{
user.ResetableData.SimRoomData.ReceivedRewardChapters.Add(new SimRoomChapterInfo()
{
Difficulty = difficulty,
Chapter = chapter
});
}
/// <summary>
/// Calculate incremental reward
/// </summary>
/// <returns>Incremental reward data</returns>
private static NetRewardData CalculateIncrementalReward(User user,
SimulationRoomChapterRecord chapter, SimulationRoomChapterRecord receivedChapterRecord)
{
var reward = new NetRewardData();
var rewardRecord = GameData.Instance.GetRewardTableEntry(chapter.RewardId);
var receivedRewardRecord = GameData.Instance.GetRewardTableEntry(receivedChapterRecord.RewardId);
if (rewardRecord?.Rewards == null || receivedRewardRecord?.Rewards == null)
{
// If rewardRecord or receivedRewardRecord is empty, return the complete reward record
reward = RewardUtils.RegisterRewardsForUser(user, rewardId: chapter.RewardId);
}
else
{
var receivedRewardIds = receivedRewardRecord.Rewards
.Where(x => x != null)
.ToDictionary(x => x.RewardId, x => x.RewardValue);
foreach (var rewardItem in rewardRecord.Rewards.Where(x => x != null))
{
int receivedAmount = receivedRewardIds.GetValueOrDefault(rewardItem.RewardId, 0);
int remainingAmount = Math.Max(0, rewardItem.RewardValue - receivedAmount);
if (remainingAmount > 0)
{
RewardUtils.AddSingleObject(user, ref reward, rewardItem.RewardId,
rewardType: rewardItem.RewardType, remainingAmount);
}
}
}
return reward;
}
}
}

View File

@@ -0,0 +1,47 @@
using EpinelPS.Utils;
using EpinelPS.Database;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/simplemode/selectbuff")]
public class SimpleModeSelectBuff : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// { "buffToAdd": 1010106, "buffToDelete": 1010105 }
ReqSelectSimRoomSimpleModeBuff req = await ReadData<ReqSelectSimRoomSimpleModeBuff>();
User user = GetUser();
List<int> buffs = user.ResetableData.SimRoomData.Buffs;
List<int> legacyBuffs = user.ResetableData.SimRoomData.LegacyBuffs;
if (req.BuffToDelete > 0)
{
if (buffs.Contains(req.BuffToDelete)) buffs.Remove(req.BuffToDelete);
if (legacyBuffs.Contains(req.BuffToDelete)) legacyBuffs.Remove(req.BuffToDelete);
}
if (req.BuffToAdd > 0)
{
if (!buffs.Contains(req.BuffToDelete)) buffs.Add(req.BuffToDelete);
if (!legacyBuffs.Contains(req.BuffToDelete)) legacyBuffs.Add(req.BuffToDelete);
}
ResSelectSimRoomSimpleModeBuff response = new()
{
Result = SimRoomResult.Reset,
NextSimpleModeBuffSelectionInfo = new()
{
RemainingBuffSelectCount = 8 - buffs.Count,
BuffOptions = { buffs },
},
};
user.ResetableData.SimRoomData.Entered = false;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,24 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/simplemode/setskipoption")]
public class SimpleModeSetSkipOption : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSetSimRoomSimpleModeSkipOption>();
// ReqSetSimRoomSimpleModeSkipOption Fields
// bool Enabled
User user = GetUser();
ResSetSimRoomSimpleModeSkipOption response = new();
user.ResetableData.SimRoomData.IsSimpleModeSkipEnabled = req.Enabled;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,41 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/simplemode/skipall")]
public class SimpleModeSkipAll : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
await ReadData<ReqSkipAllSimRoomSimpleMode>();
var user = GetUser();
// ResSkipAllSimRoomSimpleMode Fields
// SimRoomResult Result
// NetRewardData Reward
// NetRewardData RewardByRewardUpEvent
ResSkipAllSimRoomSimpleMode response = new()
{
Result = SimRoomResult.Success
};
// Reward
try
{
user.ResetableData.SimRoomData.CurrentDifficulty = 5;
user.ResetableData.SimRoomData.CurrentChapter = 3;
var reward = SimRoomHelper.SimRoomReceivedReward(user, 5, 3); // 5 = difficulty, 3 = stage
if (reward is not null) response.Reward = reward;
}
catch (Exception ex)
{
Logging.WriteLine($"SkipAllSimpleMode Reward Exception: {ex.Message}", LogType.Error);
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,25 @@
using EpinelPS.Utils;
using EpinelPS.Database;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/simplemode/skipbuffselection")]
public class SimpleModeSkipBuffSelection : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
await ReadData<ReqSkipSimRoomSimpleModeBuffSelection>();
User user = GetUser();
ResSkipSimRoomSimpleModeBuffSelection response = new()
{
Result = SimRoomResult.Reset,
};
user.ResetableData.SimRoomData.Entered = false;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,65 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Simroom
{
[PacketPath("/simroom/simplemode/start")]
public class SimpleModeStart : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
await ReadData<ReqStartSimRoomSimpleMode>();
User user = GetUser();
// ResStartSimRoomSimpleMode Fields
// SimRoomResult Result
// RepeatedField<NetSimRoomEvent> Events
// NextSimpleModeBuffSelectionInfo
ResStartSimRoomSimpleMode response = new()
{
Result = SimRoomResult.Success,
// NextSimpleModeBuffSelectionInfo = new()
// {
// BuffOptions = { user.ResetableData.SimRoomData.LegacyBuffs }
// }
};
// Events
try
{
user.ResetableData.SimRoomData.Entered = true;
var simRoomEvents = SimRoomHelper.GetSimRoomEvents(user, 5, 3); // 5 = difficulty, 3 = stage
if (simRoomEvents is not null)
{
foreach (var simRoomEvent in simRoomEvents)
{
// Check if event is battle and is first order
if (simRoomEvent.EventCase == NetSimRoomEvent.EventOneofCase.Battle && simRoomEvent.Location.Order == 1)
{
var location = simRoomEvent.Location;
if (location is null) continue;
SimRoomHelper.GetBuffOptions(user, location);
}
}
JsonDb.Save();
var userSimRoomEvents = user.ResetableData.SimRoomData.Events;
if (userSimRoomEvents is not null)
{
simRoomEvents = [.. userSimRoomEvents.Select(SimRoomHelper.MToNet)];
}
// Add events to response
response.Events.AddRange(simRoomEvents);
}
}
catch (Exception ex)
{
Logging.WriteLine($"SimpleModeStart Events Exception: {ex.Message}", LogType.Error);
}
await WriteDataAsync(response);
}
}
}

View File

@@ -115,12 +115,66 @@ namespace EpinelPS.Models
public int Defense;
public int Hp;
}
public class SimroomData
// Simroom Data
public class SimRoomData
{
public int CurrentDifficulty;
public int CurrentChapter;
public List<int> Buffs = [];
public List<int> LegacyBuffs = [];
public List<SimRoomEvent> Events = [];
public List<SimRoomCharacterHp> RemainingHps = [];
public List<SimRoomChapterInfo> ReceivedRewardChapters = [];
public bool IsSimpleModeSkipEnabled = false;
public bool Entered = false;
}
public class SimRoomEvent
{
public SimRoomEventLocationInfo Location = new();
public bool Selected;
public SimRoomBattleEvent Battle = new();
public SimRoomSelectionEvent Selection = new();
public int EventCase;
}
public class SimRoomEventLocationInfo
{
public int Chapter;
public int Stage;
public int Order;
}
public class SimRoomChapterInfo
{
public int Difficulty;
public int Chapter;
}
public class SimRoomBattleEvent
{
public int Id;
public List<int> BuffOptions = [];
public int Progress;
public int RemainingTargetHealth;
public int BuffPreviewId;
}
public class SimRoomSelectionEvent
{
public int Id;
public int SelectedNumber;
public List<SimRoomSelectionGroupElement> Group = [];
}
public class SimRoomSelectionGroupElement
{
public int SelectionNumber;
public int Id;
public bool IsDone;
public int RandomBuff;
}
public class SimRoomCharacterHp
{
public long Csn;
public int Hp;
}
public class ResetableData
{
public int WipeoutCount = 0;
@@ -128,7 +182,7 @@ namespace EpinelPS.Models
public int InterceptionTickets = 3;
public List<int> CompletedDailyMissions = [];
public int DailyMissionPoints;
public SimroomData SimRoomData = new();
public SimRoomData SimRoomData = new();
public Dictionary<int, int> DailyCounselCount = [];

View File

@@ -3,6 +3,7 @@ using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.Models;
public class ClearedTutorialData
{
public int Id;
@@ -34,6 +35,7 @@ public class User
public DateTime BanEnd;
public int BanId = 0;
public DateTime LastReset = DateTime.MinValue;
public DateTime LastWeeklyReset = DateTime.MinValue;
// Game data
public List<string> CompletedScenarios = [];
@@ -403,7 +405,36 @@ public class User
return LastReset < todayResetTime;
}
public void ResetDataIfNeeded()
private bool ShouldResetWeekly()
{
var nowLocal = DateTime.UtcNow;
// Calculate the most recent Tuesday reset time
DayOfWeek currentDay = nowLocal.DayOfWeek;
int daysSinceTuesday = ((int)currentDay - (int)DayOfWeek.Tuesday + 7) % 7;
// Get the date of the most recent Tuesday
DateTime thisTuesday = nowLocal.Date.AddDays(-daysSinceTuesday);
// Compute the weekly reset time
DateTime weeklyResetTime = new(
thisTuesday.Year,
thisTuesday.Month,
thisTuesday.Day,
JsonDb.Instance.ResetHourUtcTime, 0, 0
);
// If nowLocal is before the weekly reset time, subtract a week
if (nowLocal < weeklyResetTime)
{
weeklyResetTime = weeklyResetTime.AddDays(-7);
}
// If user's last reset was before the last scheduled 2 PM, they need reset
return LastWeeklyReset < weeklyResetTime;
}
/*public void ResetDataIfNeeded()
{
if (!ShouldResetUser()) return;
@@ -412,5 +443,41 @@ public class User
LastReset = DateTime.UtcNow;
ResetableData = new();
JsonDb.Save();
}*/
public void ResetDataIfNeeded()
{
bool needsSave = false;
// Check daily reset
if (ShouldResetUser())
{
Logging.WriteLine("Resetting daily user data...", LogType.Warning);
LastReset = DateTime.UtcNow;
ResetableData = new()
{
SimRoomData = new()
{
LegacyBuffs = ResetableData.SimRoomData.LegacyBuffs // Retain old LegacyBuffs data
}
};
needsSave = true;
}
// Check weekly reset
if (ShouldResetWeekly())
{
Logging.WriteLine("Resetting weekly user data...", LogType.Warning);
LastWeeklyReset = DateTime.UtcNow;
ResetableData = new();
needsSave = true;
}
if (needsSave)
{
JsonDb.Save();
}
}
}

View File

@@ -0,0 +1,90 @@
namespace EpinelPS.Utils
{
public static class DateTimeHelper
{
/// <summary>
/// Get the specified time of the next day in the specified time zone
/// </summary>
/// <param name="timeZoneId">Time zone ID, such as "China Standard Time", "Eastern Standard Time", "UTC"</param>
/// <param name="hour">Hours (0-23)</param>
/// <param name="minute">Minutes (0-59)</param>
/// <returns>The specified time of the next day</returns>
public static DateTime GetNextDayAtTime(string timeZoneId = "", int hour = 0, int minute = 0)
{
// Get the current time of the target time zone and the current time zone
(DateTime currentTime, TimeZoneInfo tz) = GetCurrentTimeWithZone(timeZoneId);
DateTime nextDay = new DateTime(currentTime.Year, currentTime.Month, currentTime.Day, hour, minute, 0).AddDays(1);
return TimeZoneInfo.ConvertTimeToUtc(nextDay, tz);
}
/// <summary>
/// Get the next specified day of the week + specified time in the specified time zone
/// </summary>
/// <param name="timeZoneId">Time zone ID, such as "China Standard Time"、"Eastern Standard Time"、"UTC"</param>
/// <param name="targetDay">Target day of the week, for example DayOfWeek.Monday</param>
/// <param name="hour">Hours (0-23)</param>
/// <param name="minute">Minutes (0-59)</param>
/// <returns>Next specified day of the week Specified time </returns>
public static DateTime GetNextWeekdayAtTime(string timeZoneId, DayOfWeek targetDay, int hour, int minute = 0)
{
// Get the current time of the target time zone and the current time zone
(DateTime currentTime, TimeZoneInfo tz) = GetCurrentTimeWithZone(timeZoneId);
// Calculate the number of days until the target weekday
int daysUntilTarget = ((int)targetDay - (int)currentTime.DayOfWeek + 7) % 7;
if (daysUntilTarget == 0) daysUntilTarget = 7; // If today is the target day of the week, take next week
DateTime nextWeekday = new DateTime(currentTime.Year, currentTime.Month, currentTime.Day, hour, minute, 0).AddDays(daysUntilTarget);
return TimeZoneInfo.ConvertTimeToUtc(nextWeekday, tz);
}
/// <summary>
/// Get a specific day of next month in the specified time zone at a specified time
/// </summary>
/// <param name="timeZoneId">Time zone ID, such as "China Standard Time"、"Eastern Standard Time"、"UTC"</param>
/// <param name="day">Target date (1-31, ensure that this date exists in the month)</param>
/// <param name="hour">Hours (0-23)</param>
/// <param name="minute">Minutes (0-59)</param>
/// <returns>Specified date and time next month</returns>
public static DateTime GetNextMonthDayAtTime(string timeZoneId, int day, int hour, int minute = 0)
{
// Get the current time of the target time zone and the current time zone
(DateTime currentTime, TimeZoneInfo tz) = GetCurrentTimeWithZone(timeZoneId);
// Calculate the year and month of next month
int year = currentTime.Month == 12 ? currentTime.Year + 1 : currentTime.Year;
int month = currentTime.Month == 12 ? 1 : currentTime.Month + 1;
// Ensure the date is valid (avoid situations like 30th February)
int daysInMonth = DateTime.DaysInMonth(year, month);
int targetDay = Math.Min(day, daysInMonth);
// Construct target time
DateTime target = new(year, month, targetDay, hour, minute, 0);
return TimeZoneInfo.ConvertTimeToUtc(target, tz);
}
/// <summary>
/// Get the current time and timezone object for a specified timezone
/// </summary>
/// <param name="timeZoneId">Time zone ID, such as "China Standard Time"、"Eastern Standard Time"、"UTC"</param>
/// <returns>(currentTime, tz) tuple</returns>
public static (DateTime currentTime, TimeZoneInfo tz) GetCurrentTimeWithZone(string timeZoneId)
{
// Get the target time zone
TimeZoneInfo tz = TimeZoneInfo.Local;
if (timeZoneId != null && timeZoneId != "")
tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
DateTime currentTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
return (currentTime, tz);
}
}
}

View File

@@ -1,11 +1,11 @@
{
"StaticDataMpk": {
"Url": "https://cloud.nikke-kr.com/prdenv/139-cf218bd245/staticdata/data/qa-251030-10b/473550/mpk/StaticData.pack",
"Version": "data/qa-251030-10b/473550",
"Salt1": "UfBN5TYtYG7pAY6lxoZXyA7tBOf1rdoPKfxbdB/6n0M=",
"Salt2": "zgmjiq+i9OdM6TjDHKav1JepFUaWLXRtismYpUk7lt4="
"Url": "https://cloud.nikke-kr.com/prdenv/139-c596af726e/staticdata/data/qa-251120-10b-p1/477874/mpk/StaticData.pack",
"Version": "data/qa-251120-10b-p1/477874",
"Salt1": "t8iCAEGhWUzYFk6umjwwxY6Y4IrqGQcil/rAG6M/ofw=",
"Salt2": "eBWJaf2wR/WdkfhYEc9x7gHmZX8inyrDf6sA5l7N4zk="
},
"ResourceBaseURL": "https://cloud.nikke-kr.com/prdenv/139-bfaa7caf86/{Platform}",
"ResourceBaseURL": "https://cloud.nikke-kr.com/prdenv/139-b131234cad/{Platform}",
"GameMinVer": "100.0.1",
"GameMaxVer": "150.0.2",
"TargetVersion": "139.8.13"