254 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
Mikhail Tyukin
2c1bdee666 update event data 2025-09-28 15:48:22 -04:00
Mikhail Tyukin
1f1ffdc034 use correct datatype 2025-09-28 14:20:40 -04:00
Mikhail Tyukin
775092b652 fix errors 2025-09-28 10:20:50 -04:00
Mikhail Tyukin
f470e054cf fix map loading 2025-09-27 22:41:10 -04:00
Mikhail Tyukin
29427ab515 Generate static data schema, use pascal case for it 2025-09-27 22:34:39 -04:00
Mikhail Tyukin
50d4d23e00 prepare 2025-09-27 20:49:34 -04:00
Mikhail Tyukin
17e843ecdb update protobuf 2025-09-27 17:41:40 -04:00
Mikhail Tyukin
04e661b2b7 update to 138, use mpk by default 2025-09-27 12:35:26 -04:00
fxz2018
71be17a043 Fix: SetTeam logic - Replace or add teams based on TeamNumber (#55) 2025-09-25 09:14:00 -04:00
User958568
de1b4081b2 InTheMirror Arcade's game (#56) 2025-09-21 20:02:56 -04:00
Mikhail Tyukin
996b585500 remove li pass popup 2025-09-15 21:48:45 -04:00
User958568
793e55f9de soda! (#53)
* Add files via upload

* Add files via upload

* Add files via upload
2025-09-14 11:50:49 -04:00
fxz2018
2e4310edf1 implementation for the Favorite Item ,Character Counse and Harmony Cube systems (#51)
* feat: Implement Favorite Item and Harmony Cube systems

This commit introduces the core implementation for the Favorite Item and Harmony Cube systems.

Features:

- Added data structures and loading for Favorite Items, Harmony Cubes, and Attractive Levels.

- Implemented lobby handlers for all related actions:

    - Favorite Items: equip, increase exp, quests, rewards, etc.

    - Harmony Cubes: get, clear, increase exp, level up, management, etc.

    - Character Counsel: check, present, quick counsel.

- Updated user data models to store related progression.

- Switched JSON deserialization for db.json to Newtonsoft.Json to handle protobuf models correctly.

* fix  InfraCoreExp

* fix UserFavoriteItems and present count

---------

Co-authored-by: Mikhail Tyukin <mishakeys20@gmail.com>
2025-09-09 19:05:12 -04:00
Mikhail Tyukin
ee15420257 update to v137 2025-09-08 18:50:03 -04:00
Mikhail Tyukin
bb46cf584f Revert "*untested* update game version to 137"
This reverts commit 216bc75f83.
2025-09-07 21:29:02 -04:00
Mikhail Tyukin
216bc75f83 *untested* update game version to 137 2025-09-07 21:22:49 -04:00
Mikhail Tyukin
bb603c44f3 update protobuf to 136 2025-08-09 14:05:30 -04:00
Mikhail Tyukin
c464107149 update version info 2025-08-09 13:22:12 -04:00
zhumoumoumou
74a2829773 Fix add slots in synchro device. (#49) 2025-08-03 11:51:09 -04:00
Mikhail Tyukin
689c568180 fix game version not supported when it is
this occurs when game was already patched
2025-07-23 21:02:59 -04:00
Mikhail Tyukin
5725de9fd5 fix nickname 2025-07-23 20:16:56 -04:00
Mikhail Tyukin
622c6d49ba Fix 5/7 loop 2025-07-23 13:33:09 -04:00
Mikhail Tyukin
6310916920 Move models out of JsonDb 2025-07-21 16:47:45 -04:00
Mikhail Tyukin
f217be263a Update GetCurrentTime.cs 2025-07-21 16:09:27 -04:00
Mikhail Tyukin
ba3215745c use utc time for resethour 2025-07-21 16:09:04 -04:00
Mikhail Tyukin
84766e1c34 fix namespaces, compiler warnings and messages 2025-07-21 09:08:13 -04:00
Mikhail Tyukin
0af41389ab Switch to System.Text.Json, fix stage skipping, update data 2025-07-19 16:36:05 -04:00
Mikhail Tyukin
5fd4da9d69 fix character counsel, server reset 2025-07-19 16:08:50 -04:00
Mikhail Tyukin
a143da53e6 Username->Email 2025-07-17 15:46:31 -04:00
Mikhail Tyukin
b4920e435b sync serverswitcher 2025-07-17 15:43:42 -04:00
Mikhail Tyukin
6bdcd08240 add summer event 2025-07-17 15:42:52 -04:00
Mikhail Tyukin
04a9fe29c4 initial memorypack format support 2025-07-17 13:51:49 -04:00
Mikhail Tyukin
2c5d8acc93 download new data format 2025-07-17 12:03:55 -04:00
Mikhail Tyukin
77b111519c fix bug in GetDisplayedTeam 2025-07-17 10:38:01 -04:00
Mikhail Tyukin
5b0c2c7d92 update game version to 135.8.9 2025-07-17 10:32:32 -04:00
Mikhail Tyukin
88d3061132 improve readme 2025-07-12 16:28:06 +01:00
Mikhail Tyukin
bb3e245205 Add link to todo list 2025-07-12 16:21:55 +01:00
Mikhail Tyukin
10f2a9ebf4 update event data 2025-07-07 14:20:47 +04:00
Mikhail Tyukin
86f9328eaa update resources 2025-07-06 20:19:56 +04:00
Mikhail Tyukin
b6ac385149 update version 2025-07-03 18:15:59 +04:00
Mikhail Tyukin
39ad16a2dd update static data 2025-07-02 22:12:14 +04:00
Mikhail
2b4f460087 fix 2025-07-01 18:54:51 +04:00
Ign1s_Reiga
6bf008146f Implement usetimereward, usepiece (#39)
* implement timereward

* implement usepiece

* will not grouping selectedCharacters

* remove unused field

* i forgot remove debug code

* maybe recycleroom worked

* fix Tid has 0 value

* add class research and corporation research

* delete unnecessary using

* rename args and local var

* fix receiving currency reward

* remove line from AddSingleCurrencyObject
2025-07-01 18:46:49 +04:00
Mikhail Tyukin
027f76d745 fix bugs with interception 2025-06-27 18:25:50 +04:00
Mikhail Tyukin
63495aa5e7 fix lost sector perfect reward 2025-06-25 17:30:48 +04:00
Mikhail Tyukin
08b644463b implement lost sector, fix interception 2025-06-25 13:43:01 +04:00
Mikhail Tyukin
2b0784f4db fix bonus reward 2025-06-25 11:21:22 +04:00
Mikhail Tyukin
4e568390ed Create Counsel.cs 2025-06-24 21:54:47 +04:00
Mikhail Tyukin
bd4e0d3e3c try to remove read only flag in hosts file 2025-06-24 21:36:10 +04:00
Mikhail Tyukin
7dab32c5be implement interception rewards, random item boxes 2025-06-24 21:28:57 +04:00
Mikhail Tyukin
cb1fb566af update game version, push changes 2025-06-24 19:17:53 +04:00
Mikhail Tyukin
bb7411643c begin working on interception rewards 2025-06-23 22:12:04 +04:00
Mikhail Tyukin
13086f6d19 improve compatibility with older game versions 2025-06-23 21:25:32 +04:00
Mikhail Tyukin
1ba1a89110 add update resources button in admin panel 2025-06-23 21:08:50 +04:00
Mikhail Tyukin
3bd955eebd fix hard mode, disable ads in game, update admin panel 2025-06-23 16:18:16 +04:00
Mikhail Tyukin
7955c2f4db update event data, change protobuf, improve db format 2025-06-23 11:38:18 +04:00
Mikhail Tyukin
0133f7f55b begin working on sim room 2025-06-14 15:20:19 -04:00
Mikhail Tyukin
ffbb5c792c update protobuf, add missing requests 2025-06-14 14:37:37 -04:00
Mikhail Tyukin
4592f57742 Fix looping issue 2025-06-14 11:30:01 -04:00
Mikhail Tyukin
f904dc50cb update game version to 134.8.13, push local changes 2025-06-13 09:21:01 -04:00
阁主
059821deb7 Beautiful admin panel (#35)
* Fixed some issues

Solve the problem of repeated addition of user objects in db.json after logging into the control panel

* 更新

* 更新

* 修复问题

* Update

* Add missing files
2025-05-25 08:32:34 -05:00
Mikhail Tyukin
03aca327b0 improve server selector logic 2025-05-15 15:39:13 -04:00
Mikhail Tyukin
c7ccb92659 change launcher html page 2025-05-15 15:25:43 -04:00
Mikhail Tyukin
b7425b6fde update to 133 2025-05-15 15:15:13 -04:00
Mikhail
a0b7f33c80 implement fastclearstage 2025-05-04 20:22:08 -04:00
Mikhail
24a0bc1d81 add remaining commands to admin panel 2025-05-04 17:57:39 -04:00
Mikhail
ca787a384c admin panel work 2025-05-04 17:35:11 -04:00
Mikhail
e4969c723d fix issue requiring game reinstall after game updates 2025-05-04 14:25:07 -04:00
Mikhail
b30869953f improve notice api 2025-05-03 16:10:13 -04:00
Mikhail
58750facc2 fix chapter is locked 2025-05-03 12:31:20 -04:00
Mikhail
0ba37e5d97 improve logging 2025-05-03 12:29:22 -04:00
Mikhail
9304f86603 refactoring part 3, improve logging 2025-05-03 12:20:56 -04:00
Mikhail
0b936fa6e5 refactoring part 2, use source generators for GameData 2025-05-03 11:52:09 -04:00
Mikhail
4265c43b4f refactoring part 1 2025-05-03 10:35:36 -04:00
Mikhail Tyukin
734349a297 Update README.md 2025-05-02 11:21:03 -04:00
Mikhail Tyukin
2c37167fad Update README.md 2025-05-02 11:19:52 -04:00
Mikhail
98311f73d2 implement subquests, some fixes 2025-05-02 11:14:11 -04:00
Mikhail
6298471c09 begin work on subquests 2025-05-01 19:16:47 -04:00
Mikhail
9539c0c440 Update ExecGacha.cs 2025-05-01 18:29:31 -04:00
Mikhail
e4e8f801ce Messenger implementation 2025-05-01 18:18:15 -04:00
Mikhail
24b4d11862 implement ObtainEpReward 2025-05-01 16:21:26 -04:00
Mikhail
cb27bd926a add ObtainEpReward 2025-04-30 17:14:21 -04:00
Mikhail
2c8401fea0 fix game 2025-04-30 16:36:29 -04:00
Mikhail
ba2d725dff update resources 2025-04-30 15:50:12 -04:00
Mikhail
187326cc21 Update allmsgs.cs 2025-04-30 15:48:04 -04:00
Mikhail
4fbfa3e8ec Update gameversion.json
update version to 132.10.5
2025-04-25 17:51:54 -04:00
Mikhail
652b7e0598 update resources, misc changes 2025-04-16 14:48:10 -04:00
Mikhail
56b7ffc4bd significant improvements to server selector 2025-04-09 16:28:28 -04:00
Mikhail
2709e67b6c add ChampionBadgeData stub 2025-04-08 19:11:17 -04:00
Mikhail
c9c334c2ff implement GetFieldObjectsCount, MarkNoticeRead 2025-04-08 18:58:37 -04:00
Mikhail Tyukin
e37f08c232 Disable CET 2025-04-06 17:58:19 -04:00
GreenXeMotion
fb274baebf Rework ServerSelector app UI (#27)
* Rework ServerSelector app UI

* fix border on linux

---------

Co-authored-by: Mikhail Tyukin <mishakeys20@gmail.com>
2025-04-06 17:56:44 -04:00
Mikhail Tyukin
3769b8ef58 ignore exception on cert installation 2025-03-30 22:00:24 -04:00
Mikhail Tyukin
cd15896be1 add download link to readme 2025-03-30 12:08:18 -04:00
Mikhail Tyukin
5d16214a78 Update README.md 2025-03-30 12:07:30 -04:00
Mikhail Tyukin
0e4ca4320b file got renamed 2025-03-27 16:56:38 -04:00
Mikhail Tyukin
3d3c32007c update to game version 131.10.2 2025-03-27 15:52:17 -04:00
Mikhail
d67a7d64cd add SetNicknameInTutorial 2025-02-22 09:17:54 -05:00
Mikhail
2922e59e31 update avalonia 2025-02-21 20:53:39 -05:00
Mikhail
09ad13fabd update event data, improve server selector 2025-02-21 20:51:49 -05:00
Mikhail
07fab687e7 Add event/challengestage/get 2025-02-21 18:19:52 -05:00
Mikhail
fe40ffb717 change default game path 2025-02-21 18:10:50 -05:00
Mikhail Tyukin
0e823a3bd4 update game version to 130.8.13 2025-02-21 18:02:46 -05:00
Mikhail
343b93db9e update protos 2025-01-26 21:40:26 -05:00
Mikhail
87b56abca6 update game version to v129.12.2 2025-01-26 21:36:37 -05:00
SELEKCJONER
7237e3d4a1 Update events 2025-01-03 01:42:16 +01:00
GreenXeMotion
8f65cacdda Enhance project configurations and improve error handling (#22)
* Enhance project configurations and improve error handling

- Added DebugType and NoWarn settings to project files for EpinelPS, ServerSelector, and ServerSelector.Desktop.
- Updated ServerCertificate initialization in Program.cs to read from file bytes for better reliability.
- Improved error handling in DoLimitBreak.cs and ExecuteEventGacha.cs by throwing exceptions for null references and unavailable characters, respectively.
- Refactored ColorConsoleLoggerProvider to simplify logger creation.

These changes aim to enhance debugging capabilities and ensure more robust error management across the application.

* Remove DebugType and DebugSymbols
2024-12-31 16:30:32 -05:00
Mikhail
659d99aa0b Update dotnet-desktop.yml 2024-12-29 10:50:29 -05:00
Mikhail
0fbfb36dcc Update dotnet-desktop.yml 2024-12-29 10:47:31 -05:00
Mikhail
95d5bae671 Update to .NET 9 2024-12-29 10:46:15 -05:00
Mikhail
1cb1e34674 add setnicknamefree 2024-12-29 10:44:34 -05:00
SELEKCJONER
675eda043f Probably handles increased chance in pickup gacha
And fixes command addallmaterials to actually work with ammount argument

increased chance needs testing but doesnt seem to break anything
2024-12-29 13:46:51 +01:00
Mikhail
cbd58df62f update resource, fix body labels 2024-12-28 15:05:33 -05:00
SELEKCJONER
3e21137a4c send mileage type depending on gacha type 2024-12-28 12:51:30 +01:00
Mikhail
7a4542b97b some fixes 2024-12-27 09:45:55 -05:00
SELEKCJONER
7be612ab9d Update events 2024-12-27 12:35:05 +01:00
Mikhail
2f37fa3a3f push changes 2024-12-26 16:06:38 -05:00
Mikhail
df9b52565e Update README.md 2024-12-26 15:23:17 -05:00
Mikhail
ec4ddc46b8 Implement daily, weekly, and challenge missions 2024-12-26 15:16:37 -05:00
Mikhail
9d8c47972a fix warnings 2024-12-26 13:52:10 -05:00
Mikhail
c8dc1ef511 cleanup GameData class 2024-12-26 13:30:31 -05:00
Mikhail
5ad06556d8 begin trigger implementation 2024-12-26 11:57:05 -05:00
Mikhail
506a17cf54 update game version 2024-12-26 09:19:27 -05:00
Mikhail
1fb3b922ae update data 2024-12-26 08:50:10 -05:00
Mikhail
3b5171a37c Update ExecGacha.cs 2024-12-25 17:27:03 -05:00
Mikhail
b725d461f3 show characters in advice menu 2024-12-25 17:14:00 -05:00
Mikhail
97fe2f7b3c Add basic badge support 2024-12-25 17:04:01 -05:00
Mikhail
8bb270d786 spare body labels in gacha 2024-12-25 16:28:07 -05:00
SELEKCJONER
e9bcd7adfb Add missing Frame = user.ProfileFrame 2024-12-25 16:28:49 +01:00
Mikhail
a85b3fcc83 fix user leveling, fix power calculation 2024-12-25 10:17:52 -05:00
SELEKCJONER
5ff6264e21 Very basic implementation of mission pass 2024-12-25 15:03:57 +01:00
Mikhail
684be1b26a Match behavior of official server (part 2) 2024-12-24 15:15:41 -05:00
Mikhail
f0b13ed134 Match behavior of official server better 2024-12-24 14:19:43 -05:00
Kyle873
8f28585083 should be +1, not -1 2024-12-22 04:34:29 -05:00
Kyle873
d6f9381607 handle more edge cases in equip leveling as well as using boost modules 2024-12-22 04:22:45 -05:00
Kyle873
86a8d6d78f potential boost module calc impl, not used yet
need to investigate how modules are calculated and what ids are used

also added a potential impl for if src item's exp is also added to the dest item
2024-12-22 03:11:47 -05:00
Kyle873
4b157363f8 initial equip leveling impl 2024-12-22 02:47:01 -05:00
Kyle873
2f7c12713c add new synchro slots, fix synchro state not saving 2024-12-22 00:13:38 -05:00
Kyle873
e2ca6f7ec3 sim room stuff 2024-12-21 23:45:49 -05:00
Kyle873
e2f0807ab1 reflection-based field logger for req/res msgs 2024-12-21 23:45:36 -05:00
Mikhail
3fa3915c2b move RegisterRewardsForUser to RewardUtils 2024-12-21 11:39:36 -05:00
Mikhail
c8a47a39d0 Implement tower rewards 2024-12-21 11:22:54 -05:00
Mikhail
faead3eba1 Add various missing requests 2024-12-21 11:11:07 -05:00
Mikhail
0e13c96c4d Fix compiler warnings 2024-12-21 10:47:45 -05:00
Mikhail
b7a655c169 Remove msg folder 2024-12-21 09:50:05 -05:00
Mikhail
3b05037be5 Update JsonStaticData.cs 2024-12-21 09:44:14 -05:00
SELEKCJONER
8622eb5879 Fix unable to parse 2024-12-20 22:20:36 +01:00
Mikhail
d4deb4aa49 use decimal 2024-12-20 16:05:01 -05:00
Mikhail
5391a2da3a Update GetProductList.cs 2024-12-20 15:58:58 -05:00
Mikhail
1e7de0ef8b Revert "Update GetShopProductList.cs"
This reverts commit 8030745337.
2024-12-20 15:58:43 -05:00
Mikhail
8030745337 Update GetShopProductList.cs 2024-12-20 15:53:29 -05:00
Mikhail
5028d24b5c allow cash shop to open again 2024-12-20 15:30:29 -05:00
Kyle873
b022eb688c Skill upgrade, more commands, fixes (#21)
* add BuyWallpaper

* added partial stage clear info handling, no longer fails get check

the creation of the clear info still has imperfections, so I've left it disabled for now. no point in letting it clutter the DB until the info is at least accurate in the UI.

* unfinished CP calculation stuff

still need to impl other tables (equipment), need a better way to capture outputs and compare them to retail, not really worth the excess work currently since CP calc isn't used anywhere critical right now

* AddItem command

* AddCharacter command

* finishalltutorials command

* addallmaterials command

also load the material items table

* skill upgrade impl

* update README

* potential fix for bodies not being removed properly on limit breaks and core upgrades

the client seems to be smart enough to deal with zero count item entries, so there's no real reason to remove zero count item entries from the DB

* check to make sure we have the character before adding a body

* use CreateWholeUserDataFromDbUser instead of doing a manual copy
2024-12-20 14:39:21 -05:00
Mikhail
6c58b3e137 update packages, inventory fix 2024-12-20 11:37:33 -05:00
SELEKCJONER
9877d29e79 Fix archives at least i think so 2024-12-16 00:38:37 +01:00
SELEKCJONER
5e54cfb3f3 Update events 2024-12-15 09:30:04 +01:00
Mikhail
94e6c6faa0 Merge branch 'dev' 2024-12-07 15:04:59 -05:00
Mikhail
7110cb0e50 Update Game Version 2024-12-07 10:34:52 -05:00
SELEKCJONER
4d2fcfeaa6 Update README.md
Co-authored-by: Mikhail <mishakeys20@gmail.com>
2024-11-30 15:32:11 +01:00
Mikhail
1f289c8d70 Update README.md 2024-11-28 14:47:32 -05:00
Mikhail
de1f688831 add CheckBookmarkScenarioExists and CheckScenarioExists 2024-11-21 18:04:49 -05:00
Mikhail
93dc699b1e fix GetStoryDungeon 2024-11-21 18:03:36 -05:00
Mikhail
3be38c0d2a fix error handling 2024-11-21 18:00:22 -05:00
SELEKCJONER
2e790e0861 add new version and new event
entering new event is broken for some reason
2024-11-21 23:16:28 +01:00
SELEKCJONER
2a089a035b Add 1x free gacha
Copies regular 1x behavior
2024-11-18 22:35:45 +01:00
SELEKCJONER
c8649335f6 Add support for user titles 2024-11-18 21:51:23 +01:00
SELEKCJONER
34ce89446e Add some stuff 2024-11-17 22:49:45 +01:00
SELEKCJONER
f1e6899fc9 Experimental archive support
Note archive event returns to stage 1 if hard mode is played and left unfinished
quick battle doesnt work as well
Prolouge always plays
2024-11-17 20:51:20 +01:00
SELEKCJONER
3a6cb469a6 Fill Gacha exclusion list
Adds all unobtainable characters to exclusion list in exec gacha cs which makes rates a lot more like in official

sick pulls mode till has them
2024-11-17 11:58:12 +01:00
SELEKCJONER
71ef1cf174 Basic Anomaly intercept implementation 2024-11-15 16:10:02 +01:00
SELEKCJONER
eeab17d497 Update Program.cs 2024-11-11 17:12:21 +01:00
SELEKCJONER
8078da421c add all profile frames to user 2024-11-10 17:46:38 +01:00
Mikhail
c2a50d5682 update game version 2024-11-06 15:15:31 -05:00
Mikhail
d3a0e9dcd3 add version 2024-11-03 14:59:22 -05:00
Mikhail
1d11cee520 admin panel work 2024-11-03 14:10:43 -05:00
Mikhail
419a7d0a6a Merge branch 'main' into dev 2024-11-03 14:10:13 -05:00
Mikhail
d64de8e995 Update README.md 2024-11-03 14:06:42 -05:00
Mikhail
54e4304868 Switch to GPL v3 license 2024-11-03 14:04:50 -05:00
Mikhail
a59c9a8baa Update README.md 2024-11-03 09:02:13 -05:00
Mikhail
38299bbfa0 add SeenProductOffer 2024-11-02 14:52:39 -04:00
Mikhail
bb2132de81 add gacha pity list 2024-11-02 13:42:51 -04:00
Mikhail
7170abc770 update allmsgs 2024-11-02 13:20:54 -04:00
Mikhail
f7574aeb3a add files for vscode 2024-10-30 18:14:28 -04:00
Mikhail
d4b276fc6a some fixes 2024-10-30 18:14:03 -04:00
Mikhail
f368e36c69 update event data and improve event code 2024-10-30 17:47:47 -04:00
Mikhail
6ca8f00368 Update to 2nd anniversary update 2024-10-30 16:39:56 -04:00
Mikhail
c56c65dbd3 update game version 2024-10-17 18:37:32 -04:00
SELEKCJONER
d8fdef4e5b Add missing { 2024-09-26 17:50:14 +02:00
SELEKCJONER
3c2080f393 Hopefully fix HasCharacter + show char in profile 2024-09-26 16:08:23 +02:00
Mikhail
899a56eac5 push changes 2024-09-25 20:03:27 -04:00
Mikhail
1e5c124033 fix gacha system giving already existing characters 2024-09-25 18:36:22 -04:00
Mikhail
dd81797327 Reapply "try to fix duplicate spare body entries"
This reverts commit 2c8050a19b.
2024-09-25 18:35:48 -04:00
SELEKCJONER
2c8050a19b Revert "try to fix duplicate spare body entries"
This reverts commit 6587bb8b71.
2024-09-25 20:33:05 +02:00
SELEKCJONER
6587bb8b71 try to fix duplicate spare body entries 2024-09-25 12:31:19 +02:00
SELEKCJONER
d76cd5c095 fix IsValidScenarioStage to include _s and _e
It also prevents event stages from getting added
2024-09-23 16:33:52 +02:00
Mikhail
d7d1d9b240 fix game path check 2024-09-22 13:35:16 -04:00
Mikhail
319b169bfa Fully fix limit breaks 2024-09-22 13:33:14 -04:00
Mikhail
74ddb93c0f push admin panel changes 2024-09-22 13:15:56 -04:00
SELEKCJONER
c1d0292fe5 Add experimental fix for completestage cmd
probably fixes completestage missing some entries like d_main_18af_06
2024-09-22 19:05:12 +02:00
SELEKCJONER
9f9ec8f5f8 Add SetCoreLevel command 2024-09-22 15:06:14 +02:00
SELEKCJONER
70867cc95f Fix limit break
addallcharacters adds 0 star characters as well
2024-09-22 11:30:11 +02:00
Mikhail
7ac711b027 implement client side session mangement 2024-09-21 14:45:08 -04:00
Mikhail
3f32e569d9 improve limit breaks 2024-09-21 12:37:28 -04:00
Mikhail
b7a9d0de32 Fix music saving 2024-09-21 10:46:30 -04:00
Mikhail
9e49fd31ae update event data to previous event 2024-09-19 20:36:12 -04:00
Mikhail
92b702116c v125.8.15 update 2024-09-19 19:24:15 -04:00
Atlazs
43abfe1aa8 Basic manufacturer tower implementation with progress saving. (#15)
* Basic manufacturer tower with progress saving

* Whoops
2024-09-17 20:19:42 -04:00
SELEKCJONER
bb0e0315ab Fix wrong csn saving
probably fixes profile not opening as well
2024-09-17 08:07:51 +02:00
SELEKCJONER
f6ce7d06be Add all event and favorite item wallpapers 2024-09-15 22:36:41 +02:00
SELEKCJONER
32d329eafa Small fix
fixes a few sr characters not getting max limit break and reduces size of characters list in db.json
2024-09-10 23:59:47 +02:00
SELEKCJONER
e14ede5295 add 1x pull support 2024-09-10 18:31:11 +02:00
SELEKCJONER
745e6ca164 fix command character adding 2024-09-10 01:51:59 +02:00
SELEKCJONER
10e0aa6386 add FieldItemTable back 2024-09-10 01:40:12 +02:00
SELEKCJONER
84c0d18255 Adds a lot of stuff
- new commands
SetLevel (level) - Set all characters' level (between 1 and 999 takes effect on game and server restart)

SetSkillLevel (level) - Set all characters' skill levels between 1 and 10 (takes effect on game and server restart)

addallcharacters - Add all missing characters to the selected user with default levels and skills (takes effect on game and server restart)

- partial gacha support works
only 10x adds bodies (they still cant be used and it includes normally inaccesible characters like marian)

-partial jukebox support allows for listening but not setting lobby / commanders room bgm unless manually changed in db.json
2024-09-10 01:14:00 +02:00
Mikhail
61075aae37 implement outpost reward buffs 2024-09-02 16:32:42 -04:00
Mikhail
ed3c6bb6a0 fix jukebox, begin work on outpost rewards 2024-09-02 13:26:16 -04:00
Mikhail
a68a201c13 improve logging, various qol fixes 2024-08-31 12:39:47 -04:00
Mikhail
f199ca63e0 server selector remove launcher textbox 2024-08-31 12:39:18 -04:00
553 changed files with 942575 additions and 451712 deletions

31
.editorconfig Normal file
View File

@@ -0,0 +1,31 @@
[*.cs]
# 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 8
uses: actions/setup-dotnet@v4
- name: Install .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.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\net8.0\win-x64\publish\" "${{ github.workspace }}\out\" && xcopy /s /e "${{ github.workspace }}\EpinelPS\bin\Release\net8.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/

29
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/EpinelPS/bin/Debug/net9.0/linux-x64/EpinelPS.dll",
"args": [],
"cwd": "${workspaceFolder}/EpinelPS",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

41
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/EpinelPS/EpinelPS.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/EpinelPS/EpinelPS.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/EpinelPS/EpinelPS.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

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

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,108 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using System.Text;
namespace EpinelPS.Analyzers;
[Generator]
public class LoadRecordInitializerGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Step 1: Filter for field declarations with attributes
IncrementalValueProvider<ImmutableArray<LoadFieldInfo?>> fieldDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is FieldDeclarationSyntax fds && fds.AttributeLists.Count > 0,
transform: static (ctx, _) => GetTargetFieldInfo(ctx)
)
.Where(static m => m is not null)
.Collect();
// Step 2: Generate the code
context.RegisterSourceOutput(fieldDeclarations, (spc, fieldInfos) =>
{
string source = GenerateInitializerCode(fieldInfos!);
spc.AddSource("GameDataInitializer.g.cs", SourceText.From(source, Encoding.UTF8));
});
}
private static LoadFieldInfo? GetTargetFieldInfo(GeneratorSyntaxContext context)
{
if (context.Node is not FieldDeclarationSyntax fieldDecl)
return null;
VariableDeclaratorSyntax? variable = fieldDecl.Declaration.Variables.FirstOrDefault();
if (variable == null)
return null;
if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol symbol)
return null;
if (symbol.Type is not INamedTypeSymbol namedSymbol)
return null;
foreach (AttributeData attr in symbol.GetAttributes())
{
if (attr.ConstructorArguments.Length == 2)
{
if (attr.ConstructorArguments[0].Value is not string fileName || attr.ConstructorArguments[1].Value is not string key)
return null;
return new LoadFieldInfo
{
ContainingClass = symbol.ContainingType.ToDisplayString(),
FieldName = symbol.Name,
FileName = fileName,
Key = key,
RecordTypeName = namedSymbol.TypeArguments[1].Name
};
}
}
return null;
}
private static string GenerateInitializerCode(ImmutableArray<LoadFieldInfo> fieldInfos)
{
StringBuilder sb = new();
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine();
sb.AppendLine("namespace EpinelPS.Data;");
sb.AppendLine();
sb.AppendLine("public static class GameDataInitializer");
sb.AppendLine("{");
sb.AppendLine($"\tpublic static int TotalFiles = {fieldInfos.Length};");
sb.AppendLine("\tpublic static async Task InitializeGameData(IProgress<double> progress = null)");
sb.AppendLine("\t{");
foreach (LoadFieldInfo info in fieldInfos)
{
string tempVar = $"data_{info.FieldName}";
sb.AppendLine($"\t\tvar {tempVar} = await {info.ContainingClass}.Instance.LoadZip<{info.RecordTypeName}>(\"{info.FileName}\", progress);");
sb.AppendLine($"\t\tforeach (var obj in {tempVar})");
sb.AppendLine("\t\t{");
sb.AppendLine($"\t\t\t{info.ContainingClass}.Instance.{info.FieldName}.Add(obj.{info.Key}, obj);");
sb.AppendLine("\t\t}");
}
sb.AppendLine("\t}");
sb.AppendLine("}");
return sb.ToString();
}
private class LoadFieldInfo
{
public string ContainingClass = "";
public string FieldName = "";
public string FileName = "";
public string Key = "";
public string RecordTypeName = "";
}
}

View File

@@ -11,6 +11,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerSelector.Desktop", "S
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{4BB2E77F-84A6-4644-9FB3-38E2A7DCDEC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EpinelPS.Analyzers", "EpinelPS.Analyzers\EpinelPS.Analyzers.csproj", "{E3B18A3D-B20B-447D-BCBE-12931E18B41E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -99,6 +106,30 @@ Global
{01D0A73A-A881-439D-9318-54DB5B00D6F5}.ReleaseDLL|x64.Build.0 = Release|Any CPU
{01D0A73A-A881-439D-9318-54DB5B00D6F5}.ReleaseDLL|x86.ActiveCfg = Release|Any CPU
{01D0A73A-A881-439D-9318-54DB5B00D6F5}.ReleaseDLL|x86.Build.0 = Release|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Debug|x64.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Debug|x64.Build.0 = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Debug|x86.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Debug|x86.Build.0 = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.DebugDLL|Any CPU.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.DebugDLL|Any CPU.Build.0 = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.DebugDLL|x64.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.DebugDLL|x64.Build.0 = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.DebugDLL|x86.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.DebugDLL|x86.Build.0 = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Release|Any CPU.Build.0 = Release|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Release|x64.ActiveCfg = Release|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Release|x64.Build.0 = Release|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Release|x86.ActiveCfg = Release|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.Release|x86.Build.0 = Release|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.ReleaseDLL|Any CPU.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.ReleaseDLL|Any CPU.Build.0 = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.ReleaseDLL|x64.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.ReleaseDLL|x64.Build.0 = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.ReleaseDLL|x86.ActiveCfg = Debug|Any CPU
{E3B18A3D-B20B-447D-BCBE-12931E18B41E}.ReleaseDLL|x86.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -14,11 +14,11 @@ namespace EpinelPS.Controllers
[Route("login")]
public string Login(string seq, [FromBody] LoginEndpoint2Req req)
{
foreach (var item in JsonDb.Instance.Users)
foreach (User item in JsonDb.Instance.Users)
{
if (item.Username == req.account && item.Password == req.password)
{
var tok = CreateLauncherTokenForUser(item);
AccessToken tok = CreateLauncherTokenForUser(item);
item.LastLogin = DateTime.UtcNow;
JsonDb.Save();
@@ -55,6 +55,12 @@ namespace EpinelPS.Controllers
User user = res.Item1;
AccessToken? tok = res.Item2;
if (tok == null)
{
// TODO: better error handling
return "{}";
}
// Pretend that code is valid
return "{\"account_type\":1,\"birthday\":\"1970-01\",\"email\":\"" + user.Username + "\",\"expire\":" + tok.ExpirationTime + ",\"is_receive_email\":1,\"is_receive_email_in_night\":0,\"is_receive_video\":-1,\"lang_type\":\"en\",\"msg\":\"Success\",\"nick_name\":\"\",\"phone\":\"\",\"phone_area_code\":\"\",\"privacy_policy\":\"1\",\"privacy_update_time\":1717783097,\"region\":\"724\",\"ret\":0,\"seq\":\"" + seq + "\",\"terms_of_service\":\"\",\"terms_update_time\":0,\"uid\":\"" + user.ID + "\",\"user_agreed_dt\":\"\",\"user_agreed_pp\":\"1\",\"user_agreed_tos\":\"\",\"user_name\":\"" + user.PlayerName + "\",\"username_pass_verify\":0}";
}
@@ -64,7 +70,7 @@ namespace EpinelPS.Controllers
public string RegisterAccount(string seq, [FromBody] RegisterEPReq req)
{
// check if the account already exists
foreach (var item in JsonDb.Instance.Users)
foreach (User item in JsonDb.Instance.Users)
{
if (item.Username == req.account)
{
@@ -72,10 +78,10 @@ namespace EpinelPS.Controllers
}
}
var uid = (ulong)new Random().Next(1, int.MaxValue);
ulong uid = (ulong)new Random().Next(1, int.MaxValue);
// Check if we havent generated a UID that exists
foreach (var item in JsonDb.Instance.Users)
foreach (User item in JsonDb.Instance.Users)
{
if (item.ID == uid)
{
@@ -83,20 +89,31 @@ namespace EpinelPS.Controllers
}
}
var user = new User() { ID = uid, Password = req.password, RegisterTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Username = req.account, PlayerName = "Player_" + Rng.RandomString(8) };
User user = new()
{
ID = uid,
Password = req.password,
RegisterTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
Username = req.account,
PlayerName = "Player_" + Rng.RandomString(8),
IsAdmin = JsonDb.Instance.Users.Count == 0
};
JsonDb.Instance.Users.Add(user);
var tok = CreateLauncherTokenForUser(user);
AccessToken tok = CreateLauncherTokenForUser(user);
return "{\"expire\":" + tok.ExpirationTime + ",\"is_login\":false,\"msg\":\"Success\",\"register_time\":" + user.RegisterTime + ",\"ret\":0,\"seq\":\"" + seq + "\",\"token\":\"" + tok.Token + "\",\"uid\":\"" + user.ID + "\"}";
}
public static AccessToken CreateLauncherTokenForUser(User user)
{
// TODO: implement access token expiration
AccessToken token = new() { ExpirationTime = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeSeconds() };
token.Token = Rng.RandomString(64);
token.UserID = user.ID;
AccessToken token = new()
{
ExpirationTime = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeSeconds(),
Token = Rng.RandomString(64),
UserID = user.ID
};
JsonDb.Instance.LauncherAccessTokens.Add(token);
JsonDb.Save();

View File

@@ -1,8 +1,10 @@
using EpinelPS.Database;
using EpinelPS.LobbyServer;
using EpinelPS.Controllers.AdminPanel;
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.LobbyServer.Stage;
using EpinelPS.Models.Admin;
using EpinelPS.Utils;
using Microsoft.AspNetCore.Mvc;
using Org.BouncyCastle.Asn1.X509;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
using System.Text;
@@ -12,8 +14,7 @@ namespace EpinelPS.Controllers
[ApiController]
public class AdminApiController : ControllerBase
{
public static Dictionary<string, User> AdminAuthTokens = new();
private static MD5 md5 = MD5.Create();
private static readonly MD5 md5 = MD5.Create();
[HttpPost]
[Route("login")]
@@ -23,12 +24,12 @@ namespace EpinelPS.Controllers
bool nullusernames = false;
if (b.Username != null && b.Password != null)
{
var passwordHash = Convert.ToHexString(md5.ComputeHash(Encoding.ASCII.GetBytes(b.Password))).ToLower();
foreach (var item in JsonDb.Instance.Users)
string passwordHash = Convert.ToHexString(md5.ComputeHash(Encoding.ASCII.GetBytes(b.Password))).ToLower();
foreach (User item in JsonDb.Instance.Users)
{
if (item.Username == b.Username)
if (item.Username == b.Username && item.Password != null)
{
if (item.Password.ToLower() == passwordHash)
if (item.Password.Equals(passwordHash, StringComparison.OrdinalIgnoreCase))
{
user = item;
}
@@ -37,25 +38,20 @@ namespace EpinelPS.Controllers
}
else
{
nullusernames = true;
nullusernames = true;
}
if (user == null)
{
if (nullusernames)
{
return new LoginApiResponse() { Message = "Please enter a username and password" };
}
else
{
return new LoginApiResponse() { Message = "Username or password is incorrect" };
}
return nullusernames
? new LoginApiResponse() { Message = "Please enter a username and password" }
: new LoginApiResponse() { Message = "Username or password is incorrect" };
}
else
{
if (user.IsAdmin)
{
var tok = CreateAuthToken(user);
string tok = CreateAuthToken(user);
HttpContext.Response.Cookies.Append("token", tok);
return new LoginApiResponse() { OK = true, Token = tok };
}
@@ -64,35 +60,92 @@ namespace EpinelPS.Controllers
return new LoginApiResponse() { Message = "User is not an administrator." };
}
}
}
[HttpPost("RunCmd")]
public async Task<RunCmdResponse> RunCmd([FromBody] RunCmdRequest req)
{
if (!AdminController.CheckAuth(HttpContext)) return new RunCmdResponse() { error = "bad token" };
switch (req.cmdName)
{
case "reloadDb":
JsonDb.Reload();
return RunCmdResponse.OK;
case "completestage":
return AdminCommands.CompleteStage(ulong.Parse(req.p1), req.p2);
case "addallcharacters":
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == ulong.Parse(req.p1));
if (user == null) return new RunCmdResponse() { error = "invalid user ID" };
return AdminCommands.AddAllCharacters(user);
}
case "addallmaterials":
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == ulong.Parse(req.p1));
if (user == null) return new RunCmdResponse() { error = "invalid user ID" };
return AdminCommands.AddAllMaterials(user, int.Parse(req.p2));
}
case "SetLevel":
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == ulong.Parse(req.p1));
if (user == null) return new RunCmdResponse() { error = "invalid user ID" };
return AdminCommands.SetCharacterLevel(user, int.Parse(req.p2));
}
case "SetSkillLevel":
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == ulong.Parse(req.p1));
if (user == null) return new RunCmdResponse() { error = "invalid user ID" };
return AdminCommands.SetSkillLevel(user, int.Parse(req.p2));
}
case "SetCoreLevel":
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == ulong.Parse(req.p1));
if (user == null) return new RunCmdResponse() { error = "invalid user ID" };
return AdminCommands.SetCoreLevel(user, int.Parse(req.p2));
}
case "finishalltutorials":
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == ulong.Parse(req.p1));
if (user == null) return new RunCmdResponse() { error = "invalid user ID" };
return AdminCommands.FinishAllTutorials(user);
}
case "AddCharacter":
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == ulong.Parse(req.p1));
if (user == null) return new RunCmdResponse() { error = "invalid user ID" };
return AdminCommands.AddCharacter(user, int.Parse(req.p2));
}
case "AddItem":
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == ulong.Parse(req.p1));
if (user == null) return new RunCmdResponse() { error = "invalid user ID" };
string[] s = req.p2.Split("-");
return AdminCommands.AddItem(user, int.Parse(s[0]), int.Parse(s[1]));
}
case "updateServer":
{
return await AdminCommands.UpdateResources();
}
}
return new RunCmdResponse() { error = "Not implemented" };
}
private static string CreateAuthToken(User user)
{
var tok = RandomString(128);
AdminAuthTokens.Add(tok, user);
string tok = RandomString(128);
// 只保留一个token
JsonDb.Instance.AdminAuthTokens.Clear();
JsonDb.Instance.AdminAuthTokens.Add(tok, user.ID);
JsonDb.Save();
return tok;
}
public static string RandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[new Random().Next(s.Length)]).ToArray());
}
public class LoginApiBody
{
[Required]
public string Username { get; set; } = "";
[Required]
public string Password { get; set; } = "";
}
public class LoginApiResponse
{
public string Message { get; set; } = "";
public bool OK { get; set; }
public string Token { get; set; } = "";
return new string([.. Enumerable.Repeat(chars, length).Select(static s => s[new Random().Next(s.Length)])]);
}
}
}

View File

@@ -1,58 +0,0 @@
using EpinelPS.Models;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
namespace EpinelPS.Controllers
{
[Route("admin")]
public class AdminController : Controller
{
private readonly ILogger<AdminController> _logger;
public AdminController(ILogger<AdminController> logger)
{
_logger = logger;
}
[Route("index")]
public IActionResult Index()
{
return View();
}
[Route("dashboard")]
public IActionResult Dashboard()
{
return View();
}
[Route("Events")]
public IActionResult Events()
{
return View();
}
[Route("Configuration")]
public IActionResult Configuration()
{
return View();
}
[Route("Users")]
public IActionResult Users()
{
return View();
}
[Route("Mail")]
public IActionResult Mail()
{
return View();
}
[Route("Database")]
public IActionResult Database()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@@ -0,0 +1,94 @@
using EpinelPS.Database;
using EpinelPS.Models;
using EpinelPS.Models.Admin;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Net.Security;
using System.Linq;
namespace EpinelPS.Controllers.AdminPanel
{
[Route("admin")]
public class AdminController(ILogger<AdminController> logger) : Controller
{
private readonly ILogger<AdminController> _logger = logger;
public static bool CheckAuth(HttpContext context)
{
string? token = context.Request.Cookies["token"] ?? context.Request.Headers.Authorization.ToString().Replace("Bearer ", "");
// TODO better authentication
if (JsonDb.Instance.AdminAuthTokens.TryGetValue(token, out ulong userId))
{
User? user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == userId);
if (user != null && user.IsAdmin)
return true;
}
return false;
}
[Route("dashboard")]
public IActionResult Dashboard()
{
if (!CheckAuth(HttpContext)) return Redirect("/admin/");
return View();
}
[Route("Events")]
public IActionResult Events()
{
if (!CheckAuth(HttpContext)) return Redirect("/admin/");
return View();
}
[Route("Configuration")]
public IActionResult Configuration()
{
if (!CheckAuth(HttpContext)) return Redirect("/admin/");
ServerConfiguration model = new()
{
LogType = JsonDb.Instance.LogLevel
};
return View(model);
}
[Route("Configuration"), ActionName("Configuration")]
[HttpPost]
public IActionResult ConfigurationSave([FromForm] ServerConfiguration cfg)
{
if (!CheckAuth(HttpContext)) return Redirect("/admin/");
if (!ModelState.IsValid)
return View();
JsonDb.Instance.LogLevel = cfg.LogType;
JsonDb.Save();
return View(new ServerConfiguration() { LogType = cfg.LogType });
}
[Route("Mail")]
public IActionResult Mail()
{
if (!CheckAuth(HttpContext)) return Redirect("/admin/");
return View();
}
[Route("Database")]
public IActionResult Database()
{
if (!CheckAuth(HttpContext)) return Redirect("/admin/");
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@@ -0,0 +1,132 @@
using EpinelPS.Database;
using EpinelPS.Models.Admin;
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
namespace EpinelPS.Controllers.AdminPanel
{
[Route("admin/Users")]
public class UsersController(ILogger<UsersController> logger) : Controller
{
private readonly ILogger<UsersController> _logger = logger;
private static readonly MD5 sha = MD5.Create();
public IActionResult Index()
{
if (!AdminController.CheckAuth(HttpContext)) return Redirect("/admin/");
return View(JsonDb.Instance.Users);
}
[Route("Modify/{id}")]
public IActionResult Modify(ulong id)
{
if (!AdminController.CheckAuth(HttpContext)) return Redirect("/admin/");
User? user = JsonDb.Instance.Users.Where(x => x.ID == id).FirstOrDefault();
if (user == null)
{
return NotFound();
}
return View(
new ModUserModel()
{
IsAdmin = user.IsAdmin,
IsBanned = user.IsBanned,
Nickname = user.Nickname ?? "Unknown nickname",
sickpulls = user.sickpulls,
Username = user.Username ?? "Unknown username",
ID = user.ID
}
);
}
[Route("Modify/{id}"), ActionName("Modify")]
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult DoModifyUser(ulong id, [FromForm] ModUserModel toSet)
{
if (!AdminController.CheckAuth(HttpContext)) return Redirect("/admin/");
if (!ModelState.IsValid) throw new Exception("model state invalid");
User? user = JsonDb.Instance.Users.Where(x => x.ID == id).FirstOrDefault();
if (user == null)
{
return NotFound();
}
if (string.IsNullOrEmpty(toSet.Username))
throw new Exception("username cannot be empty");
user.Username = toSet.Username;
user.IsAdmin = toSet.IsAdmin;
user.sickpulls = toSet.sickpulls;
user.IsBanned = toSet.IsBanned;
user.Nickname = toSet.Nickname;
JsonDb.Save();
return View(new ModUserModel()
{
IsAdmin = user.IsAdmin,
IsBanned = user.IsBanned,
Nickname = user.Nickname,
sickpulls = user.sickpulls,
Username = user.Username,
ID = user.ID
});
}
[Route("SetPassword/{id}")]
public IActionResult SetPassword(ulong id)
{
if (!AdminController.CheckAuth(HttpContext)) return Redirect("/admin/");
User? user = JsonDb.Instance.Users.Where(x => x.ID == id).FirstOrDefault();
if (user == null)
{
return NotFound();
}
user.Password = ""; // do not return the password
return View(user);
}
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[Route("SetPassword")]
[HttpPost, ActionName("SetPassword")]
[ValidateAntiForgeryToken]
public IActionResult SetPasswordConfirm(ulong? id)
{
if (!AdminController.CheckAuth(HttpContext)) return Redirect("/admin/");
if (id == null)
{
return NotFound();
}
string? newPw = Request.Form["PasswordHash"];
if (string.IsNullOrEmpty(newPw))
{
return BadRequest();
}
// TODO: use bcrypt
User? userToUpdate = JsonDb.Instance.Users.Where(s => s.ID == id).FirstOrDefault();
if (userToUpdate == null)
{
return NotFound();
}
userToUpdate.Password = Convert.ToHexString(sha.ComputeHash(Encoding.ASCII.GetBytes(newPw))).ToLower();
return View(userToUpdate);
}
}
}

View File

@@ -89,9 +89,20 @@ namespace EpinelPS.Controllers
[HttpPost]
[Route("fleet.repo.game.RepoSVC/GetVersion")]
public string LauncherGetVersion()
public string LauncherGetVersion([FromBody] LauncherVersionRequest? body)
{
if (body == null)
{
return "{}";
}
return System.IO.File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "gameversion.json"));
}
public class LauncherVersionRequest
{
public int game_id {get;set;}
public int branch_id { get; set; }
}
}
}

View File

@@ -1,6 +1,8 @@
using EpinelPS.Database;
using System.Reflection;
using EpinelPS.Database;
using EpinelPS.Utils;
using Microsoft.AspNetCore.Mvc;
using Org.BouncyCastle.Ocsp;
namespace EpinelPS.Controllers
{
@@ -22,16 +24,16 @@ namespace EpinelPS.Controllers
[Route("auth/login")]
public string AuthLogin(string seq, [FromBody] LoginEndpoint1Req req)
{
foreach (var tok in JsonDb.Instance.LauncherAccessTokens)
foreach (AccessToken tok in JsonDb.Instance.LauncherAccessTokens)
{
if (tok.Token == req.channel_info.account_token)
{
var user = JsonDb.Instance.Users.Find(x => x.ID == tok.UserID);
User? user = JsonDb.Instance.Users.Find(x => x.ID == tok.UserID);
if (user != null)
{
// todo: they use another token here, but we will reuse the same one.
// todo: use a class for this, this is a mess
return "{\"birthday\":\"1970-01\",\"channel_info\":{\"account\":\"" + user.Username + "\",\"account_plat_type\":131,\"account_token\":\"" + req.channel_info.account_token + "\",\"account_type\":1,\"account_uid\":\"" + user.ID + "\",\"expire_ts\":1721667004,\"is_login\":true,\"lang_type\":\"en\",\"phone_area_code\":\"\",\"token\":\"" + req.channel_info.account_token + "\"},\"del_account_info\":\"{\\\"ret\\\":0,\\\"msg\\\":\\\"\\\",\\\"status\\\":0,\\\"created_at\\\":\\\"0\\\",\\\"target_destroy_at\\\":\\\"0\\\",\\\"destroyed_at\\\":\\\"0\\\",\\\"err_code\\\":0,\\\"seq\\\":\\\"1719075066-0339089836-025921-1161847390\\\"}\",\"del_account_status\":0,\"del_li_account_status\":0,\"email\":\"" + user.Username + "\",\"extra_json\":{\"del_li_account_info\":\"{\\\"ret\\\":0,\\\"msg\\\":\\\"\\\",\\\"status\\\":0,\\\"created_at\\\":\\\"0\\\",\\\"target_destroy_at\\\":\\\"0\\\",\\\"destroyed_at\\\":\\\"0\\\",\\\"err_code\\\":0,\\\"seq\\\":\\\"" + seq + "\\\"}\",\"get_status_rsp\":{\"adult_age\":14,\"adult_age_map\":{},\"adult_check_status\":1,\"adult_check_status_expiration\":\"0\",\"adult_status_map\":{},\"certificate_type\":3,\"email\":\"\",\"eu_user_agree_status\":0,\"game_grade\":0,\"game_grade_map\":{},\"is_dma\":true,\"is_eea\":false,\"is_need_li_cert\":false,\"msg\":\"success\",\"need_parent_control\":0,\"need_realname_auth\":0,\"parent_certificate_status\":0,\"parent_certificate_status_expiration\":\"0\",\"parent_control_map\":{},\"qr_code_ret\":0,\"realname_auth_status\":0,\"region\":\"724\",\"ret\":0,\"ts\":\"1719075065\"},\"need_notify_rsp\":{\"game_sacc_openid\":\"\",\"game_sacc_uid\":\"\",\"has_game_sacc_openid\":false,\"has_game_sacc_uid\":false,\"has_li_openid\":true,\"has_li_uid\":true,\"is_receive_email\":1,\"is_receive_email_in_night\":0,\"li_openid\":\"" + user.ID + "\",\"li_uid\":\"2752409592679849\",\"need_notify\":false,\"user_agreed_game_dma\":\"2\",\"user_agreed_game_pp\":\"1\",\"user_agreed_game_tos\":\"1\",\"user_agreed_li_dt\":\"\",\"user_agreed_li_pp\":\"1\",\"user_agreed_li_tos\":\"\"}},\"first_login\":0,\"gender\":0,\"msg\":\"success\",\"need_name_auth\":false,\"openid\":\"" + user.ID + "\",\"pf\":\"LevelInfinite_LevelInfinite-Windows-windows-Windows-LevelInfinite-09af79d65d6e4fdf2d2569f0d365739d-" + user.ID + "\",\"pf_key\":\"abc\",\"picture_url\":\"\",\"reg_channel_dis\":\"Windows\",\"ret\":0,\"seq\":\"29080-2d28ea26-d71f-4822-9118-0156f1e2dba4-1719075060-99\",\"token\":\"" + tok.Token + "\",\"token_expire_time\":" + tok.ExpirationTime + ",\"uid\":\"" + user.ID + "\",\"user_name\":\"" + user.PlayerName + "\"}";
return "{\"birthday\":\"1970-01\",\"channel_info\":{\"account\":\"" + user.Username + "\",\"account_plat_type\":131,\"account_token\":\"" + req.channel_info.account_token + "\",\"account_type\":1,\"account_uid\":\"" + user.ID + "\",\"expire_ts\":1721667004,\"is_login\":true,\"lang_type\":\"en\",\"phone_area_code\":\"\",\"token\":\"" + req.channel_info.account_token + "\"},\"del_account_info\":\"{\\\"ret\\\":0,\\\"msg\\\":\\\"\\\",\\\"status\\\":0,\\\"created_at\\\":\\\"0\\\",\\\"target_destroy_at\\\":\\\"0\\\",\\\"destroyed_at\\\":\\\"0\\\",\\\"err_code\\\":0,\\\"seq\\\":\\\"1719075066-0339089836-025921-1161847390\\\"}\",\"del_account_status\":0,\"del_li_account_status\":0,\"email\":\"" + user.Username + "\",\"extra_json\":{\"del_li_account_info\":\"{\\\"ret\\\":0,\\\"msg\\\":\\\"\\\",\\\"status\\\":0,\\\"created_at\\\":\\\"0\\\",\\\"target_destroy_at\\\":\\\"0\\\",\\\"destroyed_at\\\":\\\"0\\\",\\\"err_code\\\":0,\\\"seq\\\":\\\"" + seq + "\\\"}\",\"get_status_rsp\":{\"adult_age\":14,\"adult_age_map\":{},\"adult_check_status\":1,\"adult_check_status_expiration\":\"0\",\"adult_status_map\":{},\"certificate_type\":3,\"email\":\"\",\"eu_user_agree_status\":0,\"game_grade\":0,\"game_grade_map\":{},\"is_dma\":true,\"is_eea\":false,\"is_need_li_cert\":false,\"msg\":\"success\",\"need_parent_control\":0,\"need_realname_auth\":0,\"parent_certificate_status\":0,\"parent_certificate_status_expiration\":\"0\",\"parent_control_map\":{},\"qr_code_ret\":0,\"realname_auth_status\":0,\"region\":\"724\",\"ret\":0,\"ts\":\"1719075065\"},\"need_notify_rsp\":{\"game_sacc_openid\":\"\",\"game_sacc_uid\":\"\",\"has_game_sacc_openid\":false,\"has_game_sacc_uid\":false,\"has_li_openid\":true,\"has_li_uid\":true,\"is_receive_email\":1,\"is_receive_email_in_night\":0,\"li_openid\":\"" + user.ID + "\",\"li_uid\":\"2752409592679849\",\"need_notify\":false,\"user_agreed_game_dma\":\"2\",\"user_agreed_game_pp\":\"1\",\"user_agreed_game_tos\":\"1\",\"user_agreed_li_dt\":\"\",\"user_agreed_li_pp\":\"1\",\"user_agreed_li_tos\":\"1\"}},\"first_login\":0,\"gender\":0,\"msg\":\"success\",\"need_name_auth\":false,\"openid\":\"" + user.ID + "\",\"pf\":\"LevelInfinite_LevelInfinite-Windows-windows-Windows-LevelInfinite-09af79d65d6e4fdf2d2569f0d365739d-" + user.ID + "\",\"pf_key\":\"abc\",\"picture_url\":\"\",\"reg_channel_dis\":\"Windows\",\"ret\":0,\"seq\":\"29080-2d28ea26-d71f-4822-9118-0156f1e2dba4-1719075060-99\",\"token\":\"" + tok.Token + "\",\"token_expire_time\":" + tok.ExpirationTime + ",\"uid\":\"" + user.ID + "\",\"user_name\":\"" + user.PlayerName + "\"}";
}
break;
@@ -44,10 +46,14 @@ namespace EpinelPS.Controllers
[HttpPost]
[Route("auth/auto_login")]
public string AutoLogin(string seq)
public string AutoLogin(string seq, [FromBody] AuthPkt2 req)
{
User? user;
if ((user = NetUtils.GetUser(req.token).Item1) == null) return BadAuthToken;
return "{\"del_account_info\":\"{\\\"ret\\\":0,\\\"msg\\\":\\\"\\\",\\\"status\\\":0,\\\"created_at\\\":\\\"0\\\",\\\"target_destroy_at\\\":\\\"0\\\",\\\"destroyed_at\\\":\\\"0\\\",\\\"err_code\\\":0,\\\"seq\\\":\\\"" + seq + "\\\"}\",\"del_account_status\":0,\"del_li_account_status\":0,\"extra_json\":{\"del_li_account_info\":\"{\\\"ret\\\":0,\\\"msg\\\":\\\"\\\",\\\"status\\\":0,\\\"created_at\\\":\\\"0\\\",\\\"target_destroy_at\\\":\\\"0\\\",\\\"destroyed_at\\\":\\\"0\\\",\\\"err_code\\\":0,\\\"seq\\\":\\\"" + seq + "\\\"}\",\"get_status_msg\":\"success\",\"get_status_ret\":0,\"get_status_rsp\":{\"adult_age\":14,\"adult_age_map\":{},\"adult_check_status\":1,\"adult_check_status_expiration\":\"0\",\"adult_status_map\":{},\"certificate_type\":3,\"email\":\"\",\"eu_user_agree_status\":0,\"game_grade\":0,\"game_grade_map\":{},\"is_dma\":true,\"is_eea\":false,\"is_need_li_cert\":false,\"msg\":\"success\",\"need_parent_control\":0,\"need_realname_auth\":0,\"parent_certificate_status\":0,\"parent_certificate_status_expiration\":\"0\",\"parent_control_map\":{},\"qr_code_ret\":0,\"realname_auth_status\":0,\"region\":\"724\",\"ret\":0,\"ts\":\"" + DateTimeOffset.UtcNow.ToUnixTimeSeconds()
+ "\"},\"need_notify_msg\":\"success\",\"need_notify_ret\":0,\"need_notify_rsp\":{\"has_bind_li\":true,\"is_receive_email\":1,\"is_receive_email_in_night\":0,\"user_agreed_game_dma\":\"2\",\"user_agreed_game_pp\":\"1\",\"user_agreed_game_tos\":\"1\",\"user_agreed_li_dt\":\"\",\"user_agreed_li_pp\":\"1\",\"user_agreed_li_tos\":\"\"}},\"msg\":\"success\",\"ret\":0,\"seq\":\"" + seq + "\"}";
+ "\"},\"need_notify_msg\":\"success\",\"need_notify_ret\":0,\"need_notify_rsp\":{\"has_bind_li\":true,\"is_receive_email\":1,\"is_receive_email_in_night\":0,\"user_agreed_game_dma\":\"2\",\"user_agreed_game_pp\":\"1\",\"user_agreed_game_tos\":\"1\",\"user_agreed_li_dt\":\"\",\"user_agreed_li_pp\":\"1\",\"user_agreed_li_tos\":\"1\"}},\"msg\":\"success\",\"ret\":0,\"seq\":\"" + seq + "\"}";
}
[HttpPost]
@@ -75,7 +81,7 @@ namespace EpinelPS.Controllers
if ((user = NetUtils.GetUser(req.channel_info.token).Item1) == null) return BadAuthToken;
// Pretend that code is valid
return "{\"game_sacc_openid\":\"\",\"game_sacc_uid\":\"\",\"has_game_sacc_openid\":false,\"has_game_sacc_uid\":false,\"has_li_openid\":false,\"has_li_uid\":true,\"is_receive_email\":-1,\"is_receive_email_in_night\":-1,\"li_openid\":\"\",\"li_uid\":\"" + user.ID + "\",\"msg\":\"success\",\"need_notify\":false,\"ret\":0,\"seq\":\"" + seq + "\",\"user_agreed_game_dma\":\"\",\"user_agreed_game_pp\":\"\",\"user_agreed_game_tos\":\"\",\"user_agreed_li_dt\":\"\",\"user_agreed_li_pp\":\"\",\"user_agreed_li_tos\":\"\"}";
return "{\"game_sacc_openid\":\"\",\"game_sacc_uid\":\"\",\"has_game_sacc_openid\":false,\"has_game_sacc_uid\":false,\"has_li_openid\":false,\"has_li_uid\":true,\"is_receive_email\":-1,\"is_receive_email_in_night\":-1,\"li_openid\":\"\",\"li_uid\":\"" + user.ID + "\",\"msg\":\"success\",\"need_notify\":false,\"ret\":0,\"seq\":\"" + seq + "\",\"user_agreed_game_dma\":\"\",\"user_agreed_game_pp\":\"\",\"user_agreed_game_tos\":\"\",\"user_agreed_li_dt\":\"\",\"user_agreed_li_pp\":\"\",\"user_agreed_li_tos\":\"1\"}";
}
@@ -94,12 +100,61 @@ namespace EpinelPS.Controllers
// Enable encryption, not used in this server.
return "{\"msg\":\"success\",\"ret\":0,\"seq\":\"" + seq + "\"}";
}
private static IntlNotice CreateNotice(int id, NoticeType type, string contentText, string title = "", string picture = "")
{
IntlNotice notice = new()
{
app_id = "3001001",
app_notice_id = "post-" + id,
area_list = "[\"81\",\"82\",\"83\",\"84\",\"85\"]",
extra_data = "{\"NoticeType\":\"" + type.ToString() + "\",\"Order\":\"11\",\"extra_reserved\":\"{\\\"Author\\\":\\\"\\\",\\\"Category\\\":\\\"\\\",\\\"CreateType\\\":\\\"4\\\",\\\"IsOpenService\\\":\\\"0\\\",\\\"IsToping\\\":true,\\\"Keyword\\\":\\\"\\\",\\\"Sort\\\":\\\"\\\",\\\"TopEnd\\\":\\\"2030-01-01 00:00:01\\\",\\\"TopStart\\\":\\\"2000-01-01 00:00:01\\\"}\"}",
id = id,
start_time = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
end_time = (int)DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeSeconds(),
update_time = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
status = 1
};
ContentList content = new()
{
app_content_id = "post-" + id,
content = contentText,
extra_data = "{}",
id = id,
lang_type = "en",
title = title,
update_time = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
if (!string.IsNullOrEmpty(picture))
{
content.picture_list.Add(new PictureList()
{
extra_data = "{\"id\":\"TitleImage\"}",
hash = "",
redirect_url = "",
url = picture
});
}
notice.content_list.Add(content);
return notice;
}
[HttpPost]
[Route("notice/get_notice_content")]
public string GetNotices(string seq)
public IntlNoticeListResponse GetNotices(string seq)
{
return "{\r\n \"msg\": \"success\",\r\n \"notice_list\": [\r\n {\r\n \"app_id\": \"3001001\",\r\n \"app_notice_id\": \"post-6rpvwgrdx1b\",\r\n \"area_list\": \"[\\\"81\\\",\\\"82\\\",\\\"83\\\",\\\"84\\\",\\\"85\\\"]\",\r\n \"content_list\": [\r\n {\r\n \"app_content_id\": \"post-9ilpu79xxzp\",\r\n \"content\": \"This isn't working\",\r\n \"extra_data\": \"{}\",\r\n \"id\": 48706,\r\n \"lang_type\": \"en\",\r\n \"picture_list\": [\r\n {\r\n \"extra_data\": \"{\\\"id\\\":\\\"TitleImage\\\"}\",\r\n \"hash\": \"44a99a61152b5b80a0466ff9f0cee2bc\",\r\n \"redirect_url\": \"\",\r\n \"url\": \"pnt-console-cdn.playernetwork.intlgame.com/prod/29080/notice/022681b1121a40259a575fbe587651b4.jpg\"\r\n }\r\n ],\r\n \"title\": \"New Character\",\r\n \"update_time\": 1717637493\r\n }\r\n ],\r\n \"end_time\": 1819431999,\r\n \"extra_data\": \"{\\\"NoticeType\\\":\\\"Event\\\",\\\"Order\\\":\\\"11\\\",\\\"extra_reserved\\\":\\\"{\\\\\\\"Author\\\\\\\":\\\\\\\"\\\\\\\",\\\\\\\"Category\\\\\\\":\\\\\\\"\\\\\\\",\\\\\\\"CreateType\\\\\\\":\\\\\\\"4\\\\\\\",\\\\\\\"IsOpenService\\\\\\\":\\\\\\\"0\\\\\\\",\\\\\\\"IsToping\\\\\\\":true,\\\\\\\"Keyword\\\\\\\":\\\\\\\"\\\\\\\",\\\\\\\"Sort\\\\\\\":\\\\\\\"\\\\\\\",\\\\\\\"TopEnd\\\\\\\":\\\\\\\"2030-01-01 00:00:01\\\\\\\",\\\\\\\"TopStart\\\\\\\":\\\\\\\"2000-01-01 00:00:01\\\\\\\"}\\\"}\",\r\n \"id\": 7560,\r\n \"picture_list\": [],\r\n \"start_time\": 1717617599,\r\n \"status\": 1,\r\n \"update_time\": 1717637494\r\n }\r\n ],\r\n \"ret\": 0,\r\n \"seq\": \"" + seq + "\"\r\n}";
IntlNoticeListResponse rsp = new()
{
seq = seq,
ret = 0,
msg = "success"
};
rsp.notice_list.Add(CreateNotice(2, NoticeType.System, "You are running EpinelPS v" + Assembly.GetExecutingAssembly().GetName().Version, "Server version"));
return rsp;
}
[HttpPost]
@@ -118,12 +173,24 @@ namespace EpinelPS.Controllers
[HttpPost]
[Route("profile/get_bind_info")]
public string GetProfileBindInfo(string seq, [FromBody] AuthPkt req)
public string GetProfileBindInfo(string seq, [FromBody] AuthPkt2 req)
{
User? user;
if ((user = NetUtils.GetUser(req.channel_info.token).Item1) == null) return BadAuthToken;
if ((user = NetUtils.GetUser(req.token).Item1) == null) return BadAuthToken;
return "{\"bind_list\":[{\"bind_ts\":1717783095,\"channel_info\":{\"birthday\":\"1970-01\",\"email\":\"" + user.Username + "\",\"is_receive_email\":1,\"lang_type\":\"en\",\"last_login_time\":171000000,\"nick_name\":\"\",\"phone\":\"\",\"phone_area_code\":\"\",\"region\":\"724\",\"register_account\":\"" + user.Username + "\",\"register_account_type\":1,\"register_time\":" + user.RegisterTime + ",\"seq\":\"" + seq + "\",\"uid\":\"2752409592679849\",\"user_name\":\"" + user.PlayerName + "\",\"username_pass_verify\":0},\"channelid\":131,\"email\":\"" + user.Username + "\",\"history_scopes\":[],\"is_primary\":1,\"picture_url\":\"\",\"user_name\":\"" + user.PlayerName + "\"}],\"create_ts\":" + user.RegisterTime + ",\"last_login_ts\":171000000,\"msg\":\"success\",\"ret\":0,\"seq\":\"" + seq + "\"}";
}
[HttpPost]
[Route("auth/refresh_sacc_token")]
public string RefreshAuthToken(string seq, [FromBody] AuthPkt2 req)
{
// TODO redo auth token system
AccessToken? user;
if ((user = NetUtils.GetUser(req.token).Item2) == null) return BadAuthToken;
user.ExpirationTime = DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeSeconds();
return "{\"msg\":\"success\",\"ret\":0,\"seq\":\"" + seq + "\"}";
}
}
}

View File

@@ -1,5 +1,7 @@
using EpinelPS.LobbyServer;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using EpinelPS.Utils;
namespace EpinelPS.Controllers
{
@@ -12,7 +14,11 @@ namespace EpinelPS.Controllers
[Consumes("application/octet-stream+protobuf")]
public async Task CatchAll(string all)
{
Stopwatch st = Stopwatch.StartNew();
await LobbyHandler.DispatchSingle(HttpContext);
st.Stop();
Logging.WriteLine($"POST {HttpContext.Request.Path.Value}: {HttpContext.Response.StatusCode}", LogType.Info);
}
}
}

768
EpinelPS/Data/GameData.cs Normal file
View File

@@ -0,0 +1,768 @@
using System.Data;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using EpinelPS.Database;
using EpinelPS.Utils;
using ICSharpCode.SharpZipLib.Zip;
using MemoryPack;
using Newtonsoft.Json;
namespace EpinelPS.Data
{
public class GameData
{
private static GameData? _instance;
public static GameData Instance
{
get
{
_instance ??= BuildAsync().Result;
return _instance;
}
}
public byte[] MpkHash = [];
public int MpkSize;
private ZipFile MainZip;
private MemoryStream ZipStream;
private int totalFiles = 1;
private int currentFile;
public readonly Dictionary<string, FieldMapRecord> MapData = [];
[LoadRecord("MainQuestTable.json", "Id")]
public readonly Dictionary<int, MainQuestRecord> QuestDataRecords = [];
[LoadRecord("CampaignStageTable.json", "Id")]
public readonly Dictionary<int, CampaignStageRecord> StageDataRecords = [];
[LoadRecord("RewardTable.json", "Id")]
public readonly Dictionary<int, RewardRecord> RewardDataRecords = [];
[LoadRecord("UserExpTable.json", "Level")]
public readonly Dictionary<int, UserExpRecord> UserExpDataRecords = [];
[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 = [];
[LoadRecord("CharacterTable.json", "Id")]
public readonly Dictionary<int, CharacterRecord> CharacterTable = [];
[LoadRecord("ContentsTutorialTable.json", "Id")]
public readonly Dictionary<int, ContentsTutorialRecord> TutorialTable = [];
[LoadRecord("ItemEquipTable.json", "Id")]
public readonly Dictionary<int, ItemEquipRecord> ItemEquipTable = [];
[LoadRecord("ItemMaterialTable.json", "Id")]
public readonly Dictionary<int, ItemMaterialRecord> itemMaterialTable = [];
[LoadRecord("ItemEquipExpTable.json", "Id")]
public readonly Dictionary<int, ItemEquipExpRecord> itemEquipExpTable = [];
[LoadRecord("ItemEquipGradeExpTable.json", "Id")]
public readonly Dictionary<int, ItemEquipGradeExpRecord> ItemEquipGradeExpTable = [];
[LoadRecord("CharacterLevelTable.json", "Level")]
public readonly Dictionary<int, CharacterLevelRecord> LevelData = [];
[LoadRecord("TacticAcademyFunctionTable.json", "Id")]
public readonly Dictionary<int, TacticAcademyFunctionRecord> TacticAcademyLessons = [];
[LoadRecord("SIdeStoryStageTable.json", "Id")]
public readonly Dictionary<int, SideStoryStageRecord> SidestoryRewardTable = [];
[LoadRecord("FieldItemTable.json", "Id")]
public readonly Dictionary<int, FieldItemRecord> FieldItems = [];
[LoadRecord("OutpostBattleTable.json", "Id")]
public readonly Dictionary<int, OutpostBattleRecord> OutpostBattle = [];
[LoadRecord("JukeboxListTable.json", "Id")]
public readonly Dictionary<int, JukeboxListRecord> jukeboxListDataRecords = [];
[LoadRecord("JukeboxThemeTable.json", "Id")]
public readonly Dictionary<int, JukeboxThemeRecord> jukeboxThemeDataRecords = [];
[LoadRecord("GachaTypeTable.json", "Id")]
public readonly Dictionary<int, GachaTypeRecord> gachaTypes = [];
[LoadRecord("EventManagerTable.json", "Id")]
public readonly Dictionary<int, EventManagerRecord> eventManagers = [];
[LoadRecord("LiveWallpaperTable.json", "Id")]
public readonly Dictionary<int, LiveWallpaperRecord> lwptablemgrs = [];
[LoadRecord("AlbumResourceTable.json", "Id")]
public readonly Dictionary<int, AlbumResourceRecord> albumResourceRecords = [];
[LoadRecord("UserFrameTable.json", "Id")]
public readonly Dictionary<int, UserFrameRecord> userFrameTable = [];
[LoadRecord("ArchiveRecordManagerTable.json", "Id")]
public readonly Dictionary<int, ArchiveRecordManagerRecord> archiveRecordManagerTable = [];
[LoadRecord("ArchiveEventStoryTable.json", "Id")]
public readonly Dictionary<int, ArchiveEventStoryRecord> archiveEventStoryRecords = [];
[LoadRecord("ArchiveEventQuestTable.json", "Id")]
public readonly Dictionary<int, ArchiveEventQuestRecord_Raw> archiveEventQuestRecords = [];
[LoadRecord("ArchiveEventDungeonStageTable.json", "Id")]
public readonly Dictionary<int, ArchiveEventDungeonStageRecord> archiveEventDungeonStageRecords = [];
[LoadRecord("UserTitleTable.json", "Id")]
public readonly Dictionary<int, UserTitleRecord> userTitleRecords = [];
[LoadRecord("ArchiveMessengerConditionTable.json", "Id")]
public readonly Dictionary<int, ArchiveMessengerConditionRecord> archiveMessengerConditionRecords = [];
[LoadRecord("CharacterStatTable.json", "Id")]
public readonly Dictionary<int, CharacterStatRecord> characterStatTable = [];
[LoadRecord("SkillInfoTable.json", "Id")]
public readonly Dictionary<int, SkillInfoRecord> skillInfoTable = [];
[LoadRecord("CostTable.json", "Id")]
public readonly Dictionary<int, CostRecord> costTable = [];
[LoadRecord("MidasProductTable.json", "MidasProductIdProximabeta")]
public readonly Dictionary<string, MidasProductRecord> mediasProductTable = [];
[LoadRecord("TowerTable.json", "Id")]
public readonly Dictionary<int, TowerRecord> towerTable = [];
[LoadRecord("TriggerTable.json", "Id")]
public readonly Dictionary<int, TriggerRecord> TriggerTable = [];
[LoadRecord("InfraCoreGradeTable.json", "Id")]
public readonly Dictionary<int, InfraCoreGradeRecord> InfracoreTable = [];
[LoadRecord("AttractiveCounselCharacterTable.json", "NameCode")]
public readonly Dictionary<int, AttractiveCounselCharacterRecord_Raw> AttractiveCounselCharacterTable = [];
[LoadRecord("AttractiveLevelRewardTable.json", "Id")]
public readonly Dictionary<int, AttractiveLevelRewardRecord> AttractiveLevelReward = [];
[LoadRecord("AttractiveLevelTable.json", "Id")]
public readonly Dictionary<int, AttractiveLevelRecord> AttractiveLevelTable = [];
[LoadRecord("SubQuestTable.json", "Id")]
public readonly Dictionary<int, SubQuestRecord> Subquests = [];
[LoadRecord("MessengerDialogTable.json", "Id")]
public readonly Dictionary<string, MessengerDialogRecord> Messages = [];
[LoadRecord("MessengerConditionTriggerTable.json", "Id")]
public readonly Dictionary<int, MessengerConditionTriggerRecord> MessageConditions = [];
[LoadRecord("ScenarioRewardsTable.json", "ConditionId")]
public readonly Dictionary<string, ScenarioRewardsRecord> ScenarioRewards = [];
// Note: same data types are intentional
[LoadRecord("ProductOfferTable.json", "Id")]
public readonly Dictionary<int, ProductOfferRecord> ProductOffers = [];
[LoadRecord("PopupPackageListTable.json", "Id")]
public readonly Dictionary<int, PopupPackageListRecord> PopupPackages = [];
[LoadRecord("InterceptNormalTable.json", "Id")]
public readonly Dictionary<int, InterceptNormalRecord> InterceptNormal = [];
[LoadRecord("InterceptSpecialTable.json", "Id")]
public readonly Dictionary<int, InterceptSpecialRecord> InterceptSpecial = [];
[LoadRecord("ConditionRewardTable.json", "Id")]
public readonly Dictionary<int, ConditionRewardRecord> ConditionRewards = [];
[LoadRecord("ItemConsumeTable.json", "Id")]
public readonly Dictionary<int, ItemConsumeRecord> ConsumableItems = [];
[LoadRecord("ItemRandomTable.json", "Id")]
public readonly Dictionary<int, ItemRandomRecord> RandomItem = [];
[LoadRecord("LostSectorTable.json", "Id")]
public readonly Dictionary<int, LostSectorRecord> LostSector = [];
[LoadRecord("LostSectorStageTable.json", "Id")]
public readonly Dictionary<int, LostSectorStageRecord> LostSectorStages = [];
[LoadRecord("ItemPieceTable.json", "Id")]
public readonly Dictionary<int, ItemPieceRecord> PieceItems = [];
[LoadRecord("GachaGradeProbTable.json", "Id")]
public readonly Dictionary<int, GachaGradeProbRecord> GachaGradeProb = [];
[LoadRecord("GachaListProbTable.json", "Id")]
public readonly Dictionary<int, GachaListProbRecord> GachaListProb = [];
[LoadRecord("RecycleResearchStatTable.json", "Id")]
public readonly Dictionary<int, RecycleResearchStatRecord> RecycleResearchStats = [];
[LoadRecord("RecycleResearchLevelTable.json", "Id")]
public readonly Dictionary<int, RecycleResearchLevelRecord> RecycleResearchLevels = [];
// Harmony Cube Data Tables
[LoadRecord("ItemHarmonyCubeTable.json", "Id")]
public readonly Dictionary<int, ItemHarmonyCubeRecord> ItemHarmonyCubeTable = [];
[LoadRecord("ItemHarmonyCubeLevelTable.json", "Id")]
public readonly Dictionary<int, ItemHarmonyCubeLevelRecord> ItemHarmonyCubeLevelTable = [];
// Favorite Item Data Tables
[LoadRecord("FavoriteItemTable.json", "Id")]
public readonly Dictionary<int, FavoriteItemRecord> FavoriteItemTable = [];
[LoadRecord("FavoriteItemExpTable.json", "Id")]
public readonly Dictionary<int, FavoriteItemExpRecord> FavoriteItemExpTable = [];
[LoadRecord("FavoriteItemLevelTable.json", "Id")]
public readonly Dictionary<int, FavoriteItemLevelRecord> FavoriteItemLevelTable = [];
[LoadRecord("FavoriteItemProbabilityTable.json", "Id")]
public readonly Dictionary<int, FavoriteItemProbabilityRecord> FavoriteItemProbabilityTable = [];
[LoadRecord("FavoriteItemQuestTable.json", "Id")]
public readonly Dictionary<int, FavoriteItemQuestRecord> FavoriteItemQuestTable = [];
[LoadRecord("FavoriteItemQuestStageTable.json", "Id")]
public readonly Dictionary<int, FavoriteItemQuestStageRecord> FavoriteItemQuestStageTable = [];
// Tables related to PlaySoda Arcade's event.
[LoadRecord("EventPlaySodaManagerTable.json", "Id")]
public readonly Dictionary<int, EventPlaySodaManagerRecord> EventPlaySodaManagerTable = [];
[LoadRecord("EventPlaySodaStoryModeTable.json", "Id")]
public readonly Dictionary<int, EventPlaySodaStoryModeRecord> EventPlaySodaStoryModeTable = [];
[LoadRecord("EventPlaySodaChallengeModeTable.json", "Id")]
public readonly Dictionary<int, EventPlaySodaChallengeModeRecord> EventPlaySodaChallengeModeTable = [];
[LoadRecord("EventPlaySodaPointRewardTable.json", "Id")]
public readonly Dictionary<int, EventPlaySodaPointRewardRecord> EventPlaySodaPointRewardTable = [];
// Tables related to InTheMirror Arcade's event.
[LoadRecord("EventMvgQuestTable.json", "Id")]
public readonly Dictionary<int, EventMVGQuestRecord_Raw> EventMvgQuestTable = [];
[LoadRecord("EventMvgShopTable.json", "Id")]
public readonly Dictionary<int, EventMVGShopRecord_Raw> EventMvgShopTable = [];
[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();
Logging.WriteLine("Preparing");
Stopwatch stopWatch = new();
stopWatch.Start();
await Instance.Parse();
stopWatch.Stop();
Logging.WriteLine("Preparing took " + stopWatch.Elapsed);
return Instance;
}
public GameData(string mpkFilePath)
{
if (!File.Exists(mpkFilePath)) throw new ArgumentException("Static data file must exist", nameof(mpkFilePath));
// disable warnings
ZipStream = new();
byte[] rawBytes2 = File.ReadAllBytes(mpkFilePath);
MpkHash = SHA256.HashData(rawBytes2);
MpkSize = rawBytes2.Length;
LoadGameData(mpkFilePath, GameConfig.Root.StaticDataMpk);
if (MainZip == null) throw new Exception("failed to read zip file");
}
#region Data loading
private static byte[] PresharedValue = [0xCB, 0xC2, 0x1C, 0x6F, 0xF3, 0xF5, 0x07, 0xF5, 0x05, 0xBA, 0xCA, 0xD4, 0x98, 0x28, 0x84, 0x1F, 0xF0, 0xD1, 0x38, 0xC7, 0x61, 0xDF, 0xD6, 0xE6, 0x64, 0x9A, 0x85, 0x13, 0x3E, 0x1A, 0x6A, 0x0C, 0x68, 0x0E, 0x2B, 0xC4, 0xDF, 0x72, 0xF8, 0xC6, 0x55, 0xE4, 0x7B, 0x14, 0x36, 0x18, 0x3B, 0xA7, 0xD1, 0x20, 0x81, 0x22, 0xD1, 0xA9, 0x18, 0x84, 0x65, 0x13, 0x0B, 0xED, 0xA3, 0x00, 0xE5, 0xD9];
private static RSAParameters LoadParameters = new()
{
Exponent = [0x01, 0x00, 0x01],
Modulus = [0x89, 0xD6, 0x66, 0x00, 0x7D, 0xFC, 0x7D, 0xCE, 0x83, 0xA6, 0x62, 0xE3, 0x1A, 0x5E, 0x9A, 0x53, 0xC7, 0x8A, 0x27, 0xF3, 0x67, 0xC1, 0xF3, 0xD4, 0x37, 0xFE, 0x50, 0x6D, 0x38, 0x45, 0xDF, 0x7E, 0x73, 0x5C, 0xF4, 0x9D, 0x40, 0x4C, 0x8C, 0x63, 0x21, 0x97, 0xDF, 0x46, 0xFF, 0xB2, 0x0D, 0x0E, 0xDB, 0xB2, 0x72, 0xB4, 0xA8, 0x42, 0xCD, 0xEE, 0x48, 0x06, 0x74, 0x4F, 0xE9, 0x56, 0x6E, 0x9A, 0xB1, 0x60, 0x18, 0xBC, 0x86, 0x0B, 0xB6, 0x32, 0xA7, 0x51, 0x00, 0x85, 0x7B, 0xC8, 0x72, 0xCE, 0x53, 0x71, 0x3F, 0x64, 0xC2, 0x25, 0x58, 0xEF, 0xB0, 0xC9, 0x1D, 0xE3, 0xB3, 0x8E, 0xFC, 0x55, 0xCF, 0x8B, 0x02, 0xA5, 0xC8, 0x1E, 0xA7, 0x0E, 0x26, 0x59, 0xA8, 0x33, 0xA5, 0xF1, 0x11, 0xDB, 0xCB, 0xD3, 0xA7, 0x1F, 0xB1, 0xC6, 0x10, 0x39, 0xC8, 0x31, 0x1D, 0x60, 0xDB, 0x0D, 0xA4, 0x13, 0x4B, 0x2B, 0x0E, 0xF3, 0x6F, 0x69, 0xCB, 0xA8, 0x62, 0x03, 0x69, 0xE6, 0x95, 0x6B, 0x8D, 0x11, 0xF6, 0xAF, 0xD9, 0xC2, 0x27, 0x3A, 0x32, 0x12, 0x05, 0xC3, 0xB1, 0xE2, 0x81, 0x4B, 0x40, 0xF8, 0x8B, 0x8D, 0xBA, 0x1F, 0x55, 0x60, 0x2C, 0x09, 0xC6, 0xED, 0x73, 0x96, 0x32, 0xAF, 0x5F, 0xEE, 0x8F, 0xEB, 0x5B, 0x93, 0xCF, 0x73, 0x13, 0x15, 0x6B, 0x92, 0x7B, 0x27, 0x0A, 0x13, 0xF0, 0x03, 0x4D, 0x6F, 0x5E, 0x40, 0x7B, 0x9B, 0xD5, 0xCE, 0xFC, 0x04, 0x97, 0x7E, 0xAA, 0xA3, 0x53, 0x2A, 0xCF, 0xD2, 0xD5, 0xCF, 0x52, 0xB2, 0x40, 0x61, 0x28, 0xB1, 0xA6, 0xF6, 0x78, 0xFB, 0x69, 0x9A, 0x85, 0xD6, 0xB9, 0x13, 0x14, 0x6D, 0xC4, 0x25, 0x36, 0x17, 0xDB, 0x54, 0x0C, 0xD8, 0x77, 0x80, 0x9A, 0x00, 0x62, 0x83, 0xDD, 0xB0, 0x06, 0x64, 0xD0, 0x81, 0x5B, 0x0D, 0x23, 0x9E, 0x88, 0xBD],
DP = null
};
private void LoadGameData(string file, StaticData 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);
byte[] key2 = Rfc2898DeriveBytes.Pbkdf2(PresharedValue, data.GetSalt2Bytes(), 10000, HashAlgorithmName.SHA256, 32);
byte[] decryptionKey = key2[0..16];
byte[] iv = key2[16..32];
Aes aes = Aes.Create();
aes.KeySize = 128;
aes.BlockSize = 128;
aes.Mode = CipherMode.CBC;
aes.Key = decryptionKey;
aes.IV = iv;
ICryptoTransform transform = aes.CreateDecryptor();
using CryptoStream stream = new(fileStream, transform, CryptoStreamMode.Read);
using MemoryStream ms = new();
stream.CopyTo(ms);
byte[] bytes = ms.ToArray();
ZipFile zip = new(ms, false);
ZipEntry signEntry = zip.GetEntry("sign") ?? throw new Exception("error 1");
ZipEntry dataEntry = zip.GetEntry("data") ?? throw new Exception("error 2");
Stream signStream = zip.GetInputStream(signEntry);
Stream dataStream = zip.GetInputStream(dataEntry);
using MemoryStream signMs = new();
signStream.CopyTo(signMs);
using MemoryStream dataMs = new();
dataStream.CopyTo(dataMs);
dataMs.Position = 0;
RSA rsa = RSA.Create(LoadParameters);
if (!rsa.VerifyData(dataMs, signMs.ToArray(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1))
throw new Exception("error 3");
dataMs.Position = 0;
// 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];
ZipStream = new MemoryStream();
DoTransformation(val2, iv2, dataMs, ZipStream);
ZipStream.Position = 0;
MainZip = new ZipFile(ZipStream, false);
}
public static void DoTransformation(byte[] key, byte[] salt, Stream inputStream, Stream outputStream)
{
SymmetricAlgorithm aes = Aes.Create();
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
int blockSize = aes.BlockSize / 8;
if (salt.Length != blockSize)
{
throw new ArgumentException(
"Salt size must be same as block size " +
$"(actual: {salt.Length}, expected: {blockSize})");
}
byte[] counter = (byte[])salt.Clone();
Queue<byte> xorMask = new();
byte[] zeroIv = new byte[blockSize];
ICryptoTransform counterEncryptor = aes.CreateEncryptor(key, zeroIv);
int b;
while ((b = inputStream.ReadByte()) != -1)
{
if (xorMask.Count == 0)
{
byte[] counterModeBlock = new byte[blockSize];
counterEncryptor.TransformBlock(
counter, 0, counter.Length, counterModeBlock, 0);
for (int i2 = counter.Length - 1; i2 >= 0; i2--)
{
if (++counter[i2] != 0)
{
break;
}
}
foreach (byte b2 in counterModeBlock)
{
xorMask.Enqueue(b2);
}
}
byte mask = xorMask.Dequeue();
outputStream.WriteByte((byte)(((byte)b) ^ mask));
}
}
public static async Task Load()
{
string? targetFile2 = await AssetDownloadUtil.DownloadOrGetFileAsync(GameConfig.Root.StaticDataMpk.Url, CancellationToken.None) ?? throw new Exception("static data download fail");
_instance = new(targetFile2);
}
#endregion
public async Task<X[]> LoadZip<X>(string entry, IProgress<double> bar) where X : new()
{
try
{
entry = entry.Replace(".json", ".mpk");
ZipEntry fileEntry = MainZip.GetEntry(entry);
if (fileEntry == null)
{
Logging.WriteLine(entry + " does not exist in static data", LogType.Error);
return [];
}
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);
return deserializedObject;
}
catch(Exception ex)
{
Logging.WriteLine($"Failed to parse {entry}:\n{ex}\n", LogType.Error);
return [];
}
}
public async Task Parse()
{
using ProgressBar progress = new();
totalFiles = GameDataInitializer.TotalFiles;
if (totalFiles == 0) throw new Exception("Source generator failed.");
await GameDataInitializer.InitializeGameData(progress);
foreach (ZipEntry item in MainZip)
{
if (item.Name.StartsWith("FieldMapData_") && item.Name != "FieldMapData_EventMap.mpk")
{
FieldMapRecord[] x = await LoadZip<FieldMapRecord>(item.Name, progress);
foreach (FieldMapRecord map in x)
{
MapData.Add(map.Id, map);
}
}
}
// sanity checks
if (QuestDataRecords.Count == 0) throw new Exception("QuestDataRecords should not be empty");
}
public MainQuestRecord? GetMainQuestForStageClearCondition(int stage)
{
if (QuestDataRecords.Count == 0) throw new Exception("QuestDataRecords should not be empty");
foreach (KeyValuePair<int, MainQuestRecord> item in QuestDataRecords)
{
if (item.Value.ConditionId[0].ConditionId == stage)
{
return item.Value;
}
}
return null;
}
public MainQuestRecord? GetMainQuestByTableId(int tId)
{
return QuestDataRecords[tId];
}
public CampaignStageRecord? GetStageData(int stage)
{
return StageDataRecords[stage];
}
public RewardRecord? GetRewardTableEntry(int rewardId)
{
return RewardDataRecords[rewardId];
}
/// <summary>
/// Returns the level and its minimum value for XP value
/// </summary>
/// <param name="targetExp"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public (int, int) GetUserLevelFromUserExp(int targetExp)
{
int prevLevel = 0;
int prevValue = 0;
for (int i = 1; i < UserExpDataRecords.Count + 1; i++)
{
UserExpRecord item = UserExpDataRecords[i];
if (prevValue < targetExp)
{
prevLevel = item.Level;
prevValue = item.Exp;
}
else
{
return (prevLevel, prevValue);
}
}
return (-1, -1);
}
public int GetUserMinXpForLevel(int targetLevel)
{
for (int i = 1; i < UserExpDataRecords.Count + 1; i++)
{
UserExpRecord item = UserExpDataRecords[i];
if (targetLevel == item.Level)
{
return item.Exp;
}
}
return -1;
}
public IEnumerable<int> GetAllCostumes()
{
foreach (KeyValuePair<int, CharacterCostumeRecord> item in CharacterCostumeTable)
{
yield return item.Value.Id;
}
}
internal ContentsTutorialRecord GetTutorialDataById(int TableId)
{
return TutorialTable[TableId];
}
public ItemSubType GetItemSubType(int itemType)
{
// Check if it's an equipment item
if (ItemEquipTable.TryGetValue(itemType, out ItemEquipRecord? equipRecord))
{
return equipRecord.ItemSubType;
}
// Check if it's a harmony cube item
if (ItemHarmonyCubeTable.TryGetValue(itemType, out ItemHarmonyCubeRecord? harmonyCubeRecord))
{
return harmonyCubeRecord.ItemSubType;
}
// Return null if item type not found
return ItemSubType.None;
}
internal IEnumerable<int> GetStageIdsForChapter(int chapterNumber, bool normal)
{
ChapterMod mod = normal ? ChapterMod.Normal : ChapterMod.Hard;
foreach (KeyValuePair<int, CampaignStageRecord> item in StageDataRecords)
{
CampaignStageRecord data = item.Value;
int chVal = data.ChapterId - 1;
if (chapterNumber == chVal && data.ChapterMod == mod && data.StageType == StageType.Main)
{
yield return data.Id;
}
}
}
public Dictionary<int, CharacterLevelRecord> GetCharacterLevelUpData()
{
return LevelData;
}
public TacticAcademyFunctionRecord GetTacticAcademyLesson(int lessonId)
{
return TacticAcademyLessons[lessonId];
}
public IEnumerable<string> GetScenarioStageIdsForChapter(int chapterNumber)
{
return albumResourceRecords.Values.Where(record => record.TargetChapter == chapterNumber && !string.IsNullOrEmpty(record.ScenarioGroupId)).Select(record => record.ScenarioGroupId);
}
public bool IsValIdScenarioStage(string scenarioGroupId, int targetChapter, int targetStage)
{
// Only process stages that belong to the main quest
if (!scenarioGroupId.StartsWith("d_main_"))
{
return false; // Exclude stages that don't belong to the main quest
}
// Example regular stage format: "d_main_26_08"
// 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"
var matches = Regex.Matches(scenarioGroupId, @"\d+");
var parts = new List<int>();
foreach (Match match in matches)
{
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;
}
int chapter = parts[0];
int stage = parts[1];
// 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)))
{
return true;
}
return false;
}
internal string GetMapIdFromChapter(int chapter, ChapterMod mod)
{
CampaignChapterRecord data = ChapterCampaignData[chapter - 1];
if (mod == ChapterMod.Hard)
return data.HardFieldId;
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 == 0 || x.Value.ValueMax >= damage));
if (results.Any())
return results.FirstOrDefault().Value.RewardId;
else return 0;
}
public FavoriteItemQuestRecord? GetFavoriteItemQuestTableData(int questId)
{
FavoriteItemQuestTable.TryGetValue(questId, out FavoriteItemQuestRecord?data);
return data;
}
public FavoriteItemQuestStageRecord? GetFavoriteItemQuestStageData(int stageId)
{
FavoriteItemQuestStageTable.TryGetValue(stageId, out FavoriteItemQuestStageRecord? data);
return data;
}
}
public class DataTable<T>
{
public string version { get; set; } = "";
public List<T> records { get; set; } = [];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
namespace EpinelPS.Data
{
[System.AttributeUsage(System.AttributeTargets.Field)]
public class LoadRecordAttribute(string file, string primaryKey) : Attribute
{
public string File { get; set; } = file;
public string PrimaryKey { get; set; } = primaryKey;
}
}

View File

@@ -0,0 +1,6 @@
namespace EpinelPS.Database;
public class DatabaseConnection
{
}

View File

@@ -1,291 +1,13 @@
using EpinelPS.LobbyServer;
using EpinelPS.StaticInfo;
using System.Globalization;
using EpinelPS.Data;
using EpinelPS.Utils;
using Google.Protobuf;
using Newtonsoft.Json;
using Paseto;
using Paseto.Builder;
namespace EpinelPS.Database
{
public class AccessToken
{
public string Token = "";
public long ExpirationTime;
public ulong UserID;
}
public class FieldInfo
{
public List<NetFieldStageData> CompletedStages = [];
public List<NetFieldObject> CompletedObjects = [];
}
public class FieldInfoNew
{
public List<int> CompletedStages = [];
public List<NetFieldObject> CompletedObjects = [];
}
public class Character
{
public int Csn = 0;
public int Tid = 0;
public int CostumeId = 0;
public int Level = 1;
public int UltimateLevel = 1;
public int Skill1Lvl = 1;
public int Skill2Lvl = 1;
public int Grade = 0;
}
public class MainQuestData
{
public int TableId = 0;
public bool IsReceieved = false;
}
public class UserPointData
{
public int UserLevel = 1;
public int ExperiencePoint = 0;
}
public class ItemData
{
public int ItemType;
public long Csn;
public int Count;
public int Level;
public int Exp;
public int Position;
public int Corp;
public long Isn;
}
public class EventData
{
public List<string> CompletedScenarios = new();
}
public class SynchroSlot
{
/// <summary>
/// Index of slot, 1 based
/// </summary>
public int Slot;
/// <summary>
/// Character CSN
/// </summary>
public long CharacterSerialNumber;
/// <summary>
/// Time when slot cooldown expires
/// </summary>
public long AvailableAt;
}
public class User
{
// User info
public string Username = "";
public string Password = "";
public string PlayerName = "";
public ulong ID;
public long RegisterTime;
public int LastNormalStageCleared;
public int LastHardStageCleared;
public string Nickname = "SomePlayer";
public int ProfileIconId = 39900;
public bool ProfileIconIsPrism = false;
public int ProfileFrame = 1;
public bool IsAdmin = false;
public bool IsBanned = false;
public DateTime BanStart;
public DateTime BanEnd;
public int BanId = 0;
// Game data
public List<string> CompletedScenarios = [];
public Dictionary<string, FieldInfo> FieldInfo = []; // here for backwards compatibility
public Dictionary<string, FieldInfoNew> FieldInfoNew = [];
public Dictionary<string, string> MapJson = [];
public Dictionary<CurrencyType, long> Currency = new() {
{ CurrencyType.ContentStamina, 2 }
};
public List<SynchroSlot> SynchroSlots = new List<SynchroSlot>();
public bool SynchroDeviceUpgraded = false;
public int SynchroDeviceLevel = 200;
public List<ItemData> Items = new();
public List<Character> Characters = [];
public NetWholeUserTeamData RepresentationTeamData = new();
public Dictionary<int, ClearedTutorialData> ClearedTutorialData = [];
public NetWallpaperData[] WallpaperList = [];
public Dictionary<int, NetUserTeamData> UserTeams = new Dictionary<int, NetUserTeamData>();
public Dictionary<int, bool> MainQuestData = new();
public int InfraCoreExp = 0;
public int InfraCoreLvl = 1;
public UserPointData userPointData = new();
public DateTime LastLogin = DateTime.UtcNow;
public DateTime BattleTime = DateTime.UtcNow;
public NetOutpostBattleLevel OutpostBattleLevel = new() { Level = 1 };
public int GachaTutorialPlayCount = 0;
public List<int> CompletedTacticAcademyLessons = [];
public List<int> CompletedSideStoryStages = new();
public List<int> Memorial = new();
public List<int> JukeboxBgm = new();
// Event data
public Dictionary<int, EventData> EventInfo = new();
public void SetQuest(int tid, bool recievedReward)
{
if (MainQuestData.ContainsKey(tid))
{
MainQuestData[tid] = recievedReward;
return;
}
else
{
MainQuestData.Add(tid, recievedReward);
}
}
public int GenerateUniqueItemId()
{
var num = Rng.RandomId();
while (Items.Any(x => x.Isn == num))
{
num = Rng.RandomId();
}
return num;
}
public int GenerateUniqueCharacterId()
{
var num = Rng.RandomId();
while (Characters.Any(x => x.Csn == num))
{
num = Rng.RandomId();
}
return num;
}
public bool IsStageCompleted(int id, bool isNorm)
{
foreach (var item in FieldInfoNew)
{
if (item.Key.Contains("hard") && isNorm) continue;
if (item.Key.Contains("normal") && !isNorm) continue;
if (item.Value.CompletedStages.Contains(id))
{
return true;
}
}
return false;
}
public long GetCurrencyVal(CurrencyType type)
{
if (Currency.ContainsKey(type))
return Currency[type];
else
{
Currency.Add(type, 0);
return 0;
}
}
public void AddCurrency(CurrencyType type, long val)
{
if (Currency.ContainsKey(type)) Currency[type] += val;
else Currency.Add(type, val);
}
public bool SubtractCurrency(CurrencyType type, long val)
{
if (Currency.ContainsKey(type)) Currency[type] -= val;
else return false;
if (Currency[type] < 0)
{
Currency[type] += val;
return false;
}
return true;
}
public bool CanSubtractCurrency(CurrencyType type, long val)
{
if (Currency.ContainsKey(type))
{
if (Currency[type] >= val) return true;
else return false;
}
else
{
if (val == 0) return true;
else return false;
}
}
public bool HasCharacter(int c)
{
return Characters.Any(x => x.Tid == c);
}
public Character? GetCharacterBySerialNumber(long value)
{
return Characters.Where(x => x.Csn == value).FirstOrDefault();
}
internal bool GetSynchro(long csn)
{
return SynchroSlots.Where(x => x.CharacterSerialNumber == csn).Count() >= 1;
}
internal int GetCharacterLevel(int csn)
{
var c = GetCharacterBySerialNumber(csn);
if (c == null) throw new Exception("failed to lookup character");
return GetCharacterLevel(csn, c.Level);
}
internal int GetCharacterLevel(int csn, int characterLevel)
{
foreach (var item in SynchroSlots)
{
if (item.CharacterSerialNumber == csn)
{
return GetSynchroLevel();
}
}
return characterLevel;
}
internal int GetSynchroLevel()
{
if (SynchroDeviceUpgraded)
return SynchroDeviceLevel;
var highestLevelCharacters = Characters.OrderByDescending(x => x.Level).Take(5).ToList();
if (highestLevelCharacters.Count > 0)
{
return highestLevelCharacters.Last().Level;
}
else
{
return 1;
}
}
}
public class CoreInfo
{
public int DbVersion = 3;
public List<User> Users = [];
public List<AccessToken> LauncherAccessTokens = [];
public Dictionary<string, GameClientInfo> GameClientTokens = [];
public string ServerName = "<color=\"green\">Private Server</color>";
}
internal class JsonDb
{
public static CoreInfo Instance { get; internal set; }
@@ -307,79 +29,68 @@ namespace EpinelPS.Database
if (j != null)
{
Instance = j;
if (Instance.DbVersion != 5)
{
Logging.Warn("!!!WARNING!!!");
Logging.Warn("Database version is extremely out of date.");
Logging.Warn("It is recommended to delete db.json to avoid issues.");
}
if (Instance.DbVersion == 0)
if (Instance.LauncherTokenKey.Length == 0)
{
Instance.DbVersion = 1;
// In older versions, field info key used chapter number, but now difficultly is appened.
Console.WriteLine("Starting database update...");
Console.WriteLine("Launcher token key is null, generating new key");
foreach (var user in Instance.Users)
{
foreach (var f in user.FieldInfoNew.ToList())
{
var isNumeric = int.TryParse(f.Key, out int n);
if (isNumeric)
{
var val = f.Value;
user.FieldInfoNew.Remove(f.Key);
user.FieldInfoNew.Add(n + "_Normal", val);
}
}
}
Console.WriteLine("Database update completed");
var pasetoKey = new PasetoBuilder().Use(ProtocolVersion.V4, Purpose.Local)
.GenerateSymmetricKey();
Instance.LauncherTokenKey = pasetoKey.Key.ToArray();
}
else if (Instance.DbVersion == 1)
if (Instance.EncryptionTokenKey.Length == 0)
{
Console.WriteLine("Starting database update...");
// there was a bug where equipment position was not saved, so remove all items from each characters
Instance.DbVersion = 2;
foreach (var user in Instance.Users)
{
foreach (var f in user.Items.ToList())
{
f.Csn = 0;
}
}
Console.WriteLine("Database update completed");
}
else if (Instance.DbVersion == 2)
{
Console.WriteLine("Starting database update...");
// I used to use a class for FieldInfo cleared stages, but now int list is used
Instance.DbVersion = 3;
foreach (var user in Instance.Users)
{
foreach (var f in user.FieldInfo)
{
var newField = new FieldInfoNew();
foreach (var stage in f.Value.CompletedStages)
{
newField.CompletedStages.Add(stage.StageId);
}
user.FieldInfoNew.Add(f.Key, newField);
}
user.FieldInfo.Clear();
}
Console.WriteLine("Database update completed");
Console.WriteLine("EncryptionTokenKey is null, generating new key");
var pasetoKey = new PasetoBuilder().Use(ProtocolVersion.V4, Purpose.Local)
.GenerateSymmetricKey();
Instance.EncryptionTokenKey = pasetoKey.Key.ToArray();
}
Save();
Logging.SetOutputLevel(Instance.LogLevel);
ValidateDb();
Console.WriteLine("Loaded db");
Console.WriteLine("JsonDb: Loaded");
}
else
{
throw new Exception("Failed to read configuration json file");
}
}
public static void Reload()
{
if (!File.Exists(AppDomain.CurrentDomain.BaseDirectory + "/db.json"))
{
Console.WriteLine("users: warning: configuration not found, writing default data");
Instance = new CoreInfo();
Save();
}
var j = JsonConvert.DeserializeObject<CoreInfo>(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json"));
if (j != null)
{
Instance = j;
Console.WriteLine("Database reload complete.");
}
}
private static void ValidateDb()
{
// check if character level is valid
foreach (var item in Instance.Users)
foreach (var user in Instance.Users)
{
foreach (var c in item.Characters)
foreach (var c in user.Characters)
{
if (c.Level > 1000)
{
@@ -401,5 +112,43 @@ namespace EpinelPS.Database
File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "/db.json", JsonConvert.SerializeObject(Instance, Formatting.Indented));
}
}
public static int CurrentJukeboxBgm(int position)
{
var activeJukeboxBgm = new List<int>();
//important first position holds lobby bgm id and second commanders room bgm id
foreach (var user in Instance.Users)
{
if (user.JukeboxBgm == null || user.JukeboxBgm.Count == 0)
{
// this if statemet only exists becaus some weird black magic copies default value over and over
//in the file when its set in public List<int> JukeboxBgm = new List<int>();
//delete when or if it gets fixed
user.JukeboxBgm = [2, 5];
}
activeJukeboxBgm.AddRange(user.JukeboxBgm);
}
if (activeJukeboxBgm.Count == 0)
{
return 8995001;
}
position = (position == 2 && activeJukeboxBgm.Count > 1) ? 2 : 1;
return activeJukeboxBgm[position - 1];
}
public static bool IsSickPulls(User selectedUser)
{
if (selectedUser != null)
{
return selectedUser.sickpulls;
}
else
{
throw new Exception($"User not found");
}
}
}
}
}

View File

@@ -2,27 +2,42 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IncludeHttpRuleProtos>true</IncludeHttpRuleProtos>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>True</IncludeNativeLibrariesForSelfExtract>
<NoWarn>$(NoWarn);SYSLIB0057</NoWarn>
<Version>0.140.8.0</Version>
<CETCompat>false</CETCompat>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>false</DebugSymbols>
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ASodium" Version="0.6.1" />
<PackageReference Include="ASodium" Version="0.6.4" />
<PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.15.0" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.27.3" />
<PackageReference Include="Grpc.AspNetCore" Version="2.65.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Paseto.Core" Version="1.3.0" />
<PackageReference Include="PeterO.Cbor" Version="5.0.0-alpha1" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.17.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>
<PackageReference Include="MemoryPack" Version="1.21.4" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Paseto.Core" Version="1.4.1" />
<PackageReference Include="PeterO.Cbor" Version="4.5.5" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Sodium.Core" Version="1.4.0-preview.1" />
<PackageReference Include="System.Net.Http.Formatting.Extension" Version="5.2.3" />
<PackageReference Include="Sodium.Core" Version="1.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="log4net" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
@@ -34,13 +49,8 @@
</ItemGroup>
<ItemGroup>
<None Include="Views\Shared\error.cshtml" />
<None Include="wwwroot\admin\assets\login.css" />
<None Include="wwwroot\admin\assets\login.jpg" />
<None Include="wwwroot\admin\assets\style.css" />
<None Include="wwwroot\admin\dashbrd.html" />
<None Include="wwwroot\admin\**" />
<None Include="wwwroot\admin\index.html" />
<None Include="wwwroot\admin\nav.html" />
<None Include="wwwroot\nikke_launcher\index.html" />
</ItemGroup>
@@ -58,4 +68,16 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EpinelPS.Analyzers\EpinelPS.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\admin\assets\js\loginpage.js" />
<_ContentIncludedByDefault Remove="wwwroot\admin\assets\login.css" />
<_ContentIncludedByDefault Remove="wwwroot\admin\assets\login.jpg" />
<_ContentIncludedByDefault Remove="wwwroot\admin\assets\style.css" />
<_ContentIncludedByDefault Remove="wwwroot\admin\css\site.css" />
</ItemGroup>
</Project>

View File

@@ -1,520 +0,0 @@
using EpinelPS.Utils;
using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Diagnostics;
using System.Security.Cryptography;
namespace EpinelPS.StaticInfo
{
public class GameData
{
private static GameData? _instance;
public static GameData Instance
{
get
{
if (_instance == null)
{
_instance = BuildAsync().Result;
}
return _instance;
}
}
private ZipFile MainZip;
private MemoryStream ZipStream;
private Dictionary<int, MainQuestCompletionRecord> questDataRecords;
private Dictionary<int, CampaignStageRecord> stageDataRecords;
private Dictionary<int, RewardTableRecord> rewardDataRecords;
private JArray userExpDataRecords;
private Dictionary<int, CampaignChapterRecord> chapterCampaignData;
private JArray characterCostumeTable;
private Dictionary<int, CharacterRecord> characterTable;
private Dictionary<int, ClearedTutorialData> tutorialTable;
private Dictionary<int, ItemEquipRecord> itemEquipTable;
private Dictionary<string, JArray> FieldMapData = [];
private Dictionary<int, CharacterLevelData> LevelData = [];
private Dictionary<int, TacticAcademyLessonRecord> TacticAcademyLessons = [];
public Dictionary<int, int> SidestoryRewardTable = [];
public Dictionary<string, int> PositionReward = new Dictionary<string, int>();
public Dictionary<int, FieldItemRecord> FieldItems = [];
public byte[] Sha256Hash;
public int Size;
static async Task<GameData> BuildAsync()
{
await Load();
Console.WriteLine("Preparing");
var stopWatch = new Stopwatch();
stopWatch.Start();
await Instance.Parse();
stopWatch.Stop();
Console.WriteLine("Preparing took " + stopWatch.Elapsed);
return Instance;
}
public GameData(string filePath)
{
if (!File.Exists(filePath)) throw new ArgumentException("Static data file must exist", nameof(filePath));
// disable warnings
questDataRecords = new();
stageDataRecords = new();
rewardDataRecords = new();
userExpDataRecords = new();
chapterCampaignData = new();
characterCostumeTable = new();
characterTable = new();
ZipStream = new();
tutorialTable = new();
itemEquipTable = new();
var rawBytes = File.ReadAllBytes(filePath);
Sha256Hash = SHA256.HashData(rawBytes);
Size = rawBytes.Length;
LoadGameData(filePath);
if (MainZip == null) throw new Exception("failed to read zip file");
}
#region Data loading
private static byte[] PresharedValue = [0xCB, 0xC2, 0x1C, 0x6F, 0xF3, 0xF5, 0x07, 0xF5, 0x05, 0xBA, 0xCA, 0xD4, 0x98, 0x28, 0x84, 0x1F, 0xF0, 0xD1, 0x38, 0xC7, 0x61, 0xDF, 0xD6, 0xE6, 0x64, 0x9A, 0x85, 0x13, 0x3E, 0x1A, 0x6A, 0x0C, 0x68, 0x0E, 0x2B, 0xC4, 0xDF, 0x72, 0xF8, 0xC6, 0x55, 0xE4, 0x7B, 0x14, 0x36, 0x18, 0x3B, 0xA7, 0xD1, 0x20, 0x81, 0x22, 0xD1, 0xA9, 0x18, 0x84, 0x65, 0x13, 0x0B, 0xED, 0xA3, 0x00, 0xE5, 0xD9];
private static RSAParameters LoadParameters = new RSAParameters()
{
Exponent = [0x01, 0x00, 0x01],
Modulus = [0x89, 0xD6, 0x66, 0x00, 0x7D, 0xFC, 0x7D, 0xCE, 0x83, 0xA6, 0x62, 0xE3, 0x1A, 0x5E, 0x9A, 0x53, 0xC7, 0x8A, 0x27, 0xF3, 0x67, 0xC1, 0xF3, 0xD4, 0x37, 0xFE, 0x50, 0x6D, 0x38, 0x45, 0xDF, 0x7E, 0x73, 0x5C, 0xF4, 0x9D, 0x40, 0x4C, 0x8C, 0x63, 0x21, 0x97, 0xDF, 0x46, 0xFF, 0xB2, 0x0D, 0x0E, 0xDB, 0xB2, 0x72, 0xB4, 0xA8, 0x42, 0xCD, 0xEE, 0x48, 0x06, 0x74, 0x4F, 0xE9, 0x56, 0x6E, 0x9A, 0xB1, 0x60, 0x18, 0xBC, 0x86, 0x0B, 0xB6, 0x32, 0xA7, 0x51, 0x00, 0x85, 0x7B, 0xC8, 0x72, 0xCE, 0x53, 0x71, 0x3F, 0x64, 0xC2, 0x25, 0x58, 0xEF, 0xB0, 0xC9, 0x1D, 0xE3, 0xB3, 0x8E, 0xFC, 0x55, 0xCF, 0x8B, 0x02, 0xA5, 0xC8, 0x1E, 0xA7, 0x0E, 0x26, 0x59, 0xA8, 0x33, 0xA5, 0xF1, 0x11, 0xDB, 0xCB, 0xD3, 0xA7, 0x1F, 0xB1, 0xC6, 0x10, 0x39, 0xC8, 0x31, 0x1D, 0x60, 0xDB, 0x0D, 0xA4, 0x13, 0x4B, 0x2B, 0x0E, 0xF3, 0x6F, 0x69, 0xCB, 0xA8, 0x62, 0x03, 0x69, 0xE6, 0x95, 0x6B, 0x8D, 0x11, 0xF6, 0xAF, 0xD9, 0xC2, 0x27, 0x3A, 0x32, 0x12, 0x05, 0xC3, 0xB1, 0xE2, 0x81, 0x4B, 0x40, 0xF8, 0x8B, 0x8D, 0xBA, 0x1F, 0x55, 0x60, 0x2C, 0x09, 0xC6, 0xED, 0x73, 0x96, 0x32, 0xAF, 0x5F, 0xEE, 0x8F, 0xEB, 0x5B, 0x93, 0xCF, 0x73, 0x13, 0x15, 0x6B, 0x92, 0x7B, 0x27, 0x0A, 0x13, 0xF0, 0x03, 0x4D, 0x6F, 0x5E, 0x40, 0x7B, 0x9B, 0xD5, 0xCE, 0xFC, 0x04, 0x97, 0x7E, 0xAA, 0xA3, 0x53, 0x2A, 0xCF, 0xD2, 0xD5, 0xCF, 0x52, 0xB2, 0x40, 0x61, 0x28, 0xB1, 0xA6, 0xF6, 0x78, 0xFB, 0x69, 0x9A, 0x85, 0xD6, 0xB9, 0x13, 0x14, 0x6D, 0xC4, 0x25, 0x36, 0x17, 0xDB, 0x54, 0x0C, 0xD8, 0x77, 0x80, 0x9A, 0x00, 0x62, 0x83, 0xDD, 0xB0, 0x06, 0x64, 0xD0, 0x81, 0x5B, 0x0D, 0x23, 0x9E, 0x88, 0xBD],
DP = null
};
private void LoadGameData(string file)
{
using var fileStream = File.Open(file, FileMode.Open, FileAccess.Read);
var a = new Rfc2898DeriveBytes(PresharedValue, GameConfig.Root.StaticData.GetSalt2Bytes(), 10000, HashAlgorithmName.SHA256);
var key2 = a.GetBytes(32);
byte[] decryptionKey = key2[0..16];
byte[] iv = key2[16..32];
var aes = Aes.Create();
aes.KeySize = 128;
aes.BlockSize = 128;
aes.Mode = CipherMode.CBC;
aes.Key = decryptionKey;
aes.IV = iv;
var transform = aes.CreateDecryptor();
using CryptoStream stream = new CryptoStream(fileStream, transform, CryptoStreamMode.Read);
using MemoryStream ms = new MemoryStream();
stream.CopyTo(ms);
var bytes = ms.ToArray();
var zip = new ZipFile(ms, false);
var signEntry = zip.GetEntry("sign");
if (signEntry == null) throw new Exception("error 1");
var dataEntry = zip.GetEntry("data");
if (dataEntry == null) throw new Exception("error 2");
var signStream = zip.GetInputStream(signEntry);
var dataStream = zip.GetInputStream(dataEntry);
using MemoryStream signMs = new MemoryStream();
signStream.CopyTo(signMs);
using MemoryStream dataMs = new MemoryStream();
dataStream.CopyTo(dataMs);
dataMs.Position = 0;
var rsa = RSA.Create(LoadParameters);
if (!rsa.VerifyData(dataMs, signMs.ToArray(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1))
throw new Exception("error 3");
dataMs.Position = 0;
var keyDecryptor2 = new Rfc2898DeriveBytes(PresharedValue, GameConfig.Root.StaticData.GetSalt1Bytes(), 10000, HashAlgorithmName.SHA256);
var key3 = keyDecryptor2.GetBytes(32);
byte[] val2 = key3[0..16];
byte[] iv2 = key3[16..32];
ZipStream = new MemoryStream();
DoTransformation(val2, iv2, dataMs, ZipStream);
ZipStream.Position = 0;
MainZip = new ZipFile(ZipStream, false);
}
public static void DoTransformation(byte[] key, byte[] salt, Stream inputStream, Stream outputStream)
{
SymmetricAlgorithm aes = Aes.Create();
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
int blockSize = aes.BlockSize / 8;
if (salt.Length != blockSize)
{
throw new ArgumentException(
"Salt size must be same as block size " +
$"(actual: {salt.Length}, expected: {blockSize})");
}
var counter = (byte[])salt.Clone();
var xorMask = new Queue<byte>();
var zeroIv = new byte[blockSize];
ICryptoTransform counterEncryptor = aes.CreateEncryptor(key, zeroIv);
int b;
while ((b = inputStream.ReadByte()) != -1)
{
if (xorMask.Count == 0)
{
var counterModeBlock = new byte[blockSize];
counterEncryptor.TransformBlock(
counter, 0, counter.Length, counterModeBlock, 0);
for (var i2 = counter.Length - 1; i2 >= 0; i2--)
{
if (++counter[i2] != 0)
{
break;
}
}
foreach (var b2 in counterModeBlock)
{
xorMask.Enqueue(b2);
}
}
var mask = xorMask.Dequeue();
outputStream.WriteByte((byte)(((byte)b) ^ mask));
}
}
public static async Task Load()
{
var targetFile = await AssetDownloadUtil.DownloadOrGetFileAsync(GameConfig.Root.StaticData.Url, CancellationToken.None);
if (targetFile == null) throw new Exception("static data download fail");
_instance = new(targetFile);
}
#endregion
private async Task<T> LoadZip<T>(string entry, ProgressBar bar)
{
var mainQuestData = MainZip.GetEntry(entry);
if (mainQuestData == null) throw new Exception(entry + " does not exist in static data");
using StreamReader mainQuestReader = new StreamReader(MainZip.GetInputStream(mainQuestData));
var mainQuestDataString = await mainQuestReader.ReadToEndAsync();
var questdata = JsonConvert.DeserializeObject<T>(mainQuestDataString);
if (questdata == null) throw new Exception("failed to parse " + entry);
currentFile++;
bar.Report((double)currentFile / totalFiles);
return questdata;
}
private async Task<JArray> LoadZip(string entry, ProgressBar bar)
{
var mainQuestData = MainZip.GetEntry(entry);
if (mainQuestData == null) throw new Exception(entry + " does not exist in static data");
using StreamReader mainQuestReader = new StreamReader(MainZip.GetInputStream(mainQuestData));
var mainQuestDataString = await mainQuestReader.ReadToEndAsync();
var questdata = JObject.Parse(mainQuestDataString);
if (questdata == null) throw new Exception("failed to parse " + entry);
var records = (JArray?)questdata["records"];
if (records == null) throw new Exception(entry + " is missing records element");
currentFile++;
bar.Report((double)currentFile / totalFiles);
return records;
}
int totalFiles = 14;
int currentFile = 0;
public async Task Parse()
{
using var progress = new ProgressBar();
var questDataRecords = await LoadZip<MainQuestCompletionTable>("MainQuestTable.json", progress);
foreach (var obj in questDataRecords.records)
{
this.questDataRecords.Add(obj.id, obj);
}
var stageDataRecords = await LoadZip<CampaignStageTable>("CampaignStageTable.json", progress);
foreach (var obj in stageDataRecords.records)
{
this.stageDataRecords.Add(obj.id, obj);
}
var rewardDataRecords = await LoadZip<RewardTable>("RewardTable.json", progress);
foreach (var obj in rewardDataRecords.records)
{
this.rewardDataRecords.Add(obj.id, obj);
}
var chapterCampaignData = await LoadZip<CampaignChapterTable>("CampaignChapterTable.json", progress);
foreach (var obj in chapterCampaignData.records)
{
this.chapterCampaignData.Add(obj.chapter, obj);
}
userExpDataRecords = await LoadZip("UserExpTable.json", progress);
characterCostumeTable = await LoadZip("CharacterCostumeTable.json", progress);
var characterTable = await LoadZip<CharacterTable>("CharacterTable.json", progress);
foreach (var obj in characterTable.records)
{
this.characterTable.Add(obj.id, obj);
}
var tutorialTable = await LoadZip<TutorialTable>("ContentsTutorialTable.json", progress);
foreach (var obj in tutorialTable.records)
{
this.tutorialTable.Add(obj.id, obj);
}
var itemEquipTable = await LoadZip<ItemEquipTable>("ItemEquipTable.json", progress);
foreach (var obj in itemEquipTable.records)
{
this.itemEquipTable.Add(obj.id, obj);
}
var characterLevelTable = await LoadZip("CharacterLevelTable.json", progress);
foreach (JToken item in characterLevelTable)
{
var obj = item.ToObject<CharacterLevelData>();
if (obj != null)
LevelData.Add(obj.level, obj);
else
Console.WriteLine("failed to read character level table entry");
}
var tacticLessonTable = await LoadZip("TacticAcademyFunctionTable.json", progress);
foreach (JToken item in tacticLessonTable)
{
var idRaw = item["id"];
var groupidRaw = item["group_id"];
var currencyIdRaw = item["currency_id"];
var currencyValueRaw = item["currency_value"];
if (idRaw == null) throw new InvalidDataException();
if (groupidRaw == null) throw new InvalidDataException();
if (currencyIdRaw == null) throw new InvalidDataException();
if (currencyValueRaw == null) throw new InvalidDataException();
var id = idRaw.ToObject<int>();
var currencyId = currencyIdRaw.ToObject<int>();
var currencyValue = currencyValueRaw.ToObject<int>();
var groupid = groupidRaw.ToObject<int>();
var fullId = int.Parse(groupid.ToString() + id.ToString());
TacticAcademyLessons.Add(id, new TacticAcademyLessonRecord() { CurrencyId = (CurrencyType)currencyId, CurrencyValue = currencyValue, GroupId = groupid, Id = id });
}
var sideStoryTable = await LoadZip("SideStoryStageTable.json", progress);
foreach (JToken item in sideStoryTable)
{
var idRaw = item["id"];
var rewardIdRaw = item["first_clear_reward"];
if (idRaw == null) throw new InvalidDataException();
if (rewardIdRaw != null)
{
var id2 = idRaw.ToObject<int>();
var reward = rewardIdRaw.ToObject<int>();
SidestoryRewardTable.Add(id2, reward);
}
}
foreach (ZipEntry item in MainZip)
{
if (item.Name.StartsWith("CampaignMap/"))
{
var x = await LoadZip(item.Name, progress);
var items = x[0]["ItemSpawner"];
foreach (var item2 in items)
{
var id = item2["positionId"].ToObject<string>();
var reward = item2["itemId"].ToObject<int>();
if (!PositionReward.ContainsKey(id))
PositionReward.Add(id, reward);
}
}
}
var fieldItems = await LoadZip<FieldItemTable>("FieldItemTable.json", progress);
foreach (var obj in fieldItems.records)
{
FieldItems.Add(obj.id, obj);
}
}
public MainQuestCompletionRecord? GetMainQuestForStageClearCondition(int stage)
{
foreach (var item in questDataRecords)
{
if (item.Value.condition_id == stage)
{
return item.Value;
}
}
return null;
}
public MainQuestCompletionRecord? GetMainQuestByTableId(int tid)
{
return questDataRecords[tid];
}
public CampaignStageRecord? GetStageData(int stage)
{
return stageDataRecords[stage];
}
public RewardTableRecord? GetRewardTableEntry(int rewardId)
{
return rewardDataRecords[rewardId];
}
/// <summary>
/// Returns the level and its minimum value for XP value
/// </summary>
/// <param name="targetExp"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public (int, int) GetUserLevelFromUserExp(int targetExp)
{
int prevLevel = 0;
int prevValue = 0;
for (int i = 0; i < userExpDataRecords.Count; i++)
{
var item = userExpDataRecords[i];
var level = item["level"];
if (level == null) throw new Exception("expected level field in user exp table data");
int levelValue = level.ToObject<int>();
var exp = item["exp"];
if (exp == null) throw new Exception("expected exp field in user exp table data");
int expValue = exp.ToObject<int>();
if (prevValue < targetExp)
{
prevLevel = levelValue;
prevValue = expValue;
}
else
{
return (prevLevel, prevValue);
}
}
return (-1, -1);
}
public int GetUserMinXpForLevel(int targetLevel)
{
for (int i = 0; i < userExpDataRecords.Count; i++)
{
var item = userExpDataRecords[i];
var level = item["level"];
if (level == null) throw new Exception("expected level field in user exp table data");
int levelValue = level.ToObject<int>();
if (targetLevel == levelValue)
{
var exp = item["exp"];
if (exp == null) throw new Exception("expected exp field in user exp table data");
int expValue = exp.ToObject<int>();
return expValue;
}
}
return -1;
}
public int GetNormalChapterNumberFromFieldName(string field)
{
foreach (var item in chapterCampaignData)
{
if (item.Value.field_id == field)
{
return item.Value.chapter;
}
}
return -1;
}
public IEnumerable<int> GetAllCharacterTids()
{
return characterTable.Keys;
}
public IEnumerable<int> GetAllCostumes()
{
foreach (JObject item in characterCostumeTable)
{
var id = item["id"];
if (id == null) throw new Exception("expected id field in reward data");
int value = id.ToObject<int>();
yield return value;
}
}
internal ClearedTutorialData GetTutorialDataById(int TableId)
{
return tutorialTable[TableId];
}
public string? GetItemSubType(int itemType)
{
return itemEquipTable[itemType].item_sub_type;
}
internal IEnumerable<int> GetStageIdsForChapter(int chapterNumber, bool normal)
{
string mod = normal ? "Normal" : "Hard";
foreach (var item in stageDataRecords)
{
var data = item.Value;
int chVal = data.chapter_id - 1;
if (chapterNumber == chVal && data.chapter_mod == mod && data.stage_type == "Main")
{
yield return data.id;
}
}
}
public Dictionary<int, CharacterLevelData> GetCharacterLevelUpData()
{
return LevelData;
}
public TacticAcademyLessonRecord GetTacticAcademyLesson(int lessonId)
{
return TacticAcademyLessons[lessonId];
}
}
}

View File

@@ -1,150 +0,0 @@
namespace EpinelPS.StaticInfo
{
public class MainQuestCompletionRecord
{
public int id;
public int group_id;
public string category = "";
public int condition_id;
public int next_main_quest_id = 0;
public int reward_id = 0;
public int target_chapter_id;
}
public class MainQuestCompletionTable
{
public List<MainQuestCompletionRecord> records;
}
public class CampaignStageRecord
{
public int id;
public int chapter_id;
public string stage_category = "";
public int reward_id = 0;
/// <summary>
/// Can be Normal or Hard
/// </summary>
public string chapter_mod = "";
public string stage_type = "";
public string enter_scenario = "";
public string exit_scenario = "";
}
public class CampaignStageTable
{
public List<CampaignStageRecord> records;
}
public class RewardTableRecord
{
public int id;
public int user_exp;
public int character_exp;
public RewardEntry[]? rewards;
}
public class RewardTable
{
public List<RewardTableRecord> records;
}
public class RewardEntry
{
/// <summary>
/// example: 1000000
/// </summary>
public int reward_percent;
public string percent_display_type = "";
public string reward_type = "";
public int reward_id;
public int reward_value;
}
public class ClearedTutorialData
{
public int id;
public int VersionGroup = 0;
public int GroupId;
public int ClearedStageId;
public int NextId;
public bool SaveTutorial;
}
public class TutorialTable
{
public List<ClearedTutorialData> records;
}
public class CharacterLevelData
{
/// <summary>
/// level
/// </summary>
public int level;
/// <summary>
/// can be CharacterLevel or SynchroLevel
/// </summary>
public string type = "";
/// <summary>
/// amount of credits required
/// </summary>
public int gold = 0;
/// <summary>
/// amount of battle data required
/// </summary>
public int character_exp = 0;
/// <summary>
/// amount of core dust required
/// </summary>
public int character_exp2 = 0;
}
public class TacticAcademyLessonRecord
{
public CurrencyType CurrencyId;
public int CurrencyValue;
public int Id;
public int GroupId;
}
public class CampaignChapterRecord
{
public int id;
public int chapter;
public string field_id;
public string hard_field_id;
}
public class CampaignChapterTable
{
public List<CampaignChapterRecord> records;
}
public class CharacterRecord
{
public int id;
// TODO: There is more stuff here but it isn't needed yet
}
public class CharacterTable
{
public List<CharacterRecord> records;
}
public class ItemEquipRecord
{
public int id;
public string item_sub_type;
}
public class ItemEquipTable
{
public List<ItemEquipRecord> records;
}
public class FieldItemRecord
{
public int id;
public string item_type;
public int type_value;
public bool is_final_reward;
public string difficulty;
}
public class FieldItemTable
{
public List<FieldItemRecord> records;
}
}

1
EpinelPS/Global.cs Normal file
View File

@@ -0,0 +1 @@
global using EpinelPS.Models;

View File

@@ -1,14 +1,14 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Antibot
namespace EpinelPS.LobbyServer.Antibot
{
[PacketPath("/antibot/battlereportdata")]
public class BattleReportData : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqBattleReportData>();
var response = new ResBattleReportData();
ReqBattleReportData req = await ReadData<ReqBattleReportData>();
ResBattleReportData response = new();
// this is responsible for server side anticheat

View File

@@ -1,17 +1,17 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Antibot
namespace EpinelPS.LobbyServer.Antibot
{
[PacketPath("/antibot/recvdata")]
public class RecieveAntibotData : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqAntibotRecvData>();
ReqAntibotRecvData req = await ReadData<ReqAntibotRecvData>();
// I don't really care about reimplementing the server side anticheat, so return
var response = new ResAntibotRecvData();
ResAntibotRecvData response = new();
await WriteDataAsync(response);
}

View File

@@ -0,0 +1,36 @@
using EpinelPS.Utils;
using EpinelPS.Database;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/storydungeon/clearstage")]
public class ClearArchiveStage : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqClearArchiveStage req = await ReadData<ReqClearArchiveStage>(); // has fields EventId, StageId, BattleResult
int evid = req.EventId;
int stgid = req.StageId;
int result = req.BattleResult;
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 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();
// Send the response back to the client
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,18 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/storydungeon/enterstage")]
public class EnterArchiveStage : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqEnterArchiveStage req = await ReadData<ReqEnterArchiveStage>();// has fields EventId StageId TeamNumber
int evid = req.EventId;
ResEnterArchiveStage response = new();
await WriteDataAsync(response);
}
}
}

View File

@@ -1,15 +1,15 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Archive
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/bookmark/scenario/exist")]
public class CheckBookmarkScenarioExists : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqExistScenarioBookmark>();
ReqExistScenarioBookmark req = await ReadData<ReqExistScenarioBookmark>();
var response = new ResExistScenarioBookmark();
ResExistScenarioBookmark response = new();
// TODO
await WriteDataAsync(response);
}

View File

@@ -0,0 +1,37 @@
using EpinelPS.Utils;
using EpinelPS.Database;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/scenario/complete")]
public class CompleteScenario : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCompleteArchiveScenario req = await ReadData<ReqCompleteArchiveScenario>(); // req has EventId, ScenarioId, DialogType fields
int evid = req.EventId;
string scenid = req.ScenarioId;
int dialtyp = req.DialogType;
User user = GetUser();
// Ensure we are working with the user's EventInfo and not CompletedScenarios
if (!user.EventInfo.TryGetValue(evid, out EventData? evt))
{
// Create a new EventData if the event doesn't exist
evt = new EventData();
user.EventInfo[evid] = evt;
}
// Ensure the CompletedScenarios list is initialized and add the ScenarioId
if (!evt.CompletedScenarios.Contains(scenid))
{
evt.CompletedScenarios.Add(scenid);
}
JsonDb.Save();
// Prepare and send the response
ResCompleteArchiveScenario response = new();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,34 @@
using EpinelPS.Utils;
using EpinelPS.Database;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/storydungeon/fastclearstage")]
public class FastClearArchiveStage : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqFastClearArchiveStage req = await ReadData<ReqFastClearArchiveStage>();
int evid = req.EventId;
int stgid = req.StageId;
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.");
}
if (!eventData.ClearedStages.Contains(stgid))
{
eventData.ClearedStages.Add(stgid);
// Update the LastStage in EventData
eventData.LastStage = stgid;
}
JsonDb.Save();
ResFastClearArchiveStage response = new();
// Send the response back to the client
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,51 @@
using EpinelPS.Utils;
using EpinelPS.Database;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/storydungeon/get")]
public class GetArchiveStoryDungeon : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetArchiveStoryDungeon req = await ReadData<ReqGetArchiveStoryDungeon>(); // has EventId field
int evid = req.EventId;
User user = GetUser();
// Ensure the EventInfo dictionary contains the requested EventId
if (!user.EventInfo.TryGetValue(evid, out EventData? eventData))
{
eventData = new EventData
{
CompletedScenarios = [], // Initialize empty list
Diff = 0, // Default difficulty
LastStage = 0 // Default last cleared stage
};
// Create a new default entry for the missing EventId
user.EventInfo[evid] = eventData;
JsonDb.Save();
}
// Prepare the response
ResGetArchiveStoryDungeon response = new()
{
// Populate team data
TeamData = new NetUserTeamData
{
LastContentsTeamNumber = 1,
Type = 1
}
};
// Populate the last cleared stage
response.LastClearedArchiveStageList.Add(new NetLastClearedArchiveStage
{
DifficultyId = eventData.Diff,
StageId = eventData.LastStage
});
// Send the response
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,34 @@
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/get")]
public class GetArchives : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetArchiveRecord req = await ReadData<ReqGetArchiveRecord>();
ResGetArchiveRecord response = new();
// Explicitly select IDs from the records
List<int> allIds = [.. GameData.Instance.archiveRecordManagerTable.Values.Select(record => record.Id)];
// Add the IDs to the response lists
response.ArchiveRecordManagerList.AddRange(allIds);
response.UnlockedArchiveRecordList.AddRange(allIds);
// Get entries with record_type "EventQuest"
List<ArchiveRecordManagerRecord> eventQuestRecords = [.. GameData.Instance.archiveRecordManagerTable.Values.Where(record => record.RecordType == ArchiveRecordType.EventQuest)];
response.ArchiveEventQuest = new();
response.ArchiveEventQuest.UnlockedArchiveRecordManagerEventQuestIdList.AddRange(eventQuestRecords.Select(record => record.Id));
// TODO more fields
// TODO: allow unlocking
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,21 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/minigame/getdata")]
public class GetMinigameData : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetArchiveMiniGameData req = await ReadData<ReqGetArchiveMiniGameData>();
ResGetArchiveMiniGameData response = new()
{
Json = "{}"
};
// TODO
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,25 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/scenario/getnonresettable")]
public class GetNonResettable : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetNonResettableArchiveScenario req = await ReadData<ReqGetNonResettableArchiveScenario>();
ResGetNonResettableArchiveScenario response = new();
User user = GetUser();
foreach (var (evtId, evtData) in user.EventInfo)
{
if (evtId == req.EventId)
{
response.ScenarioIdList.AddRange(evtData.CompletedScenarios);
break;
}
}
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,32 @@
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/scenario/getresettable")]
public class GetResettable : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetResettableArchiveScenario req = await ReadData<ReqGetResettableArchiveScenario>();
ResGetResettableArchiveScenario response = new(); // has ScenarioIdList field that takes in strings
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,38 @@
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Archive
{
[PacketPath("/archive/messenger/get")]
public class GetArchiveMessenger : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// Read the request containing ArchiveMessengerGroupId
ReqGetArchiveMessenger req = await ReadData<ReqGetArchiveMessenger>();
int groupId = req.ArchiveMessengerGroupId;
// Initialize the response object
ResGetArchiveMessenger response = new();
// Get the relevant data from ArchiveMessengerConditionTable
GameData gameData = GameData.Instance;
if (gameData.archiveMessengerConditionRecords.TryGetValue(groupId, out ArchiveMessengerConditionRecord? conditionRecord))
{
foreach (var condition in conditionRecord.ArchiveMessengerConditionList)
{
// Add each condition as a NetArchiveMessage in the response
response.ArchiveMessageList.Add(new NetArchiveMessage
{
ConditionId = condition.ConditionId,
MessageId = conditionRecord.Tid // Correctly using tId as MessageId
});
}
}
// Write the response back
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,24 @@
using EpinelPS.Utils;
using Google.Protobuf.WellKnownTypes;
namespace EpinelPS.LobbyServer.Arena
{
[PacketPath("/arena/champion/getbadgedata")]
public class ChampionBadgeData : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetChampionArenaDataByBadge req = await ReadData<ReqGetChampionArenaDataByBadge>();
ResGetChampionArenaDataByBadge response = new()
{
// TODO
Schedule = new NetChampionArenaSchedule(),
NextSchedule = new NetChampionArenaSchedule(),
ChampionArenaContentsState = ChampionArenaContentsState.SeasonClosed,
CurrentOrLastSeasonStartAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(5))
};
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,24 @@
using EpinelPS.Utils;
using Google.Protobuf.WellKnownTypes;
namespace EpinelPS.LobbyServer.Arena
{
[PacketPath("/arena/get")]
public class GetArena : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetArena req = await ReadData<ReqGetArena>();
User user = GetUser();
ResGetArena response = new()
{
BanInfo = new NetArenaBanInfo() { Description = "Not Implemented", StartAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), EndAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow.AddYears(10)) },
User = new NetArenaData() { User = LobbyHandler.CreateWholeUserDataFromDbUser(user) }
};
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,22 @@
using EpinelPS.Utils;
using Google.Protobuf.WellKnownTypes;
namespace EpinelPS.LobbyServer.Arena
{
[PacketPath("/arena/getbaninfo")]
public class GetArenaBanInfo : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetArenaBanInfo req = await ReadData<ReqGetArenaBanInfo>();
ResGetArenaBanInfo response = new()
{
RookieArenaBanInfo = new NetArenaBanInfo() { Description = "Not Implemented", StartAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), EndAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow.AddYears(10)) },
SpecialArenaBanInfo = new NetArenaBanInfo() { Description = "Not Implemented", StartAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), EndAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow.AddYears(10)) }
};
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,23 @@
using EpinelPS.Utils;
using Google.Protobuf.WellKnownTypes;
namespace EpinelPS.LobbyServer.Arena
{
[PacketPath("/arena/champion/get")]
public class GetChampion : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetChampionArena req = await ReadData<ReqGetChampionArena>();
ResGetChampionArena response = new()
{
Schedule = new NetChampionArenaSchedule()
};
// TODO
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,23 @@
using EpinelPS.Utils;
using Google.Protobuf.WellKnownTypes;
namespace EpinelPS.LobbyServer.Arena
{
[PacketPath("/arena/special/get")]
public class GetSpecialArena : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetSpecialArena req = await ReadData<ReqGetSpecialArena>();
User user = GetUser();
ResGetSpecialArena response = new()
{
BanInfo = new NetArenaBanInfo() { Description = "Not Implemented", StartAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), EndAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow.AddYears(10)) },
User = new NetArenaData() { User = LobbyHandler.CreateWholeUserDataFromDbUser(user) }
};
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,21 @@
using EpinelPS.Utils;
using Google.Protobuf.WellKnownTypes;
namespace EpinelPS.LobbyServer.Arena
{
[PacketPath("/arena/special/showreward")]
public class ShowSpecialArenaReward : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqShowSpecialArenaReward req = await ReadData<ReqShowSpecialArenaReward>();
ResShowSpecialArenaReward response = new()
{
IsBan = true,
BanInfo = new NetArenaBanInfo() { Description = "Not Implemented", StartAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow), EndAt = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow.AddYears(10)) }
};
await WriteDataAsync(response);
}
}
}

View File

@@ -1,16 +1,16 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Auth
namespace EpinelPS.LobbyServer.Auth
{
[PacketPath("/auth/logout")]
public class AuthLogout : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqLogout>();
ReqLogout req = await ReadData<ReqLogout>();
JsonDb.Instance.GameClientTokens.Remove(UsedAuthToken);
// TODO remove UsedAuthToken
await WriteDataAsync(new ResLogout());
}

View File

@@ -0,0 +1,63 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Paseto.Builder;
using Paseto;
using System.Text.Json;
namespace EpinelPS.LobbyServer.Auth
{
[PacketPath("/auth/enterserver")]
public class GetUserOnlineStateLog : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqEnterServer req = await ReadData<ReqEnterServer>();
// request has auth token
UsedAuthToken = req.AuthToken;
foreach (AccessToken item in JsonDb.Instance.LauncherAccessTokens)
{
if (item.Token == UsedAuthToken)
{
UserId = item.UserID;
}
}
if (UserId == 0) throw new BadHttpRequestException("unknown auth token", 403);
User user = GetUser();
GameClientInfo rsp = LobbyHandler.GenGameClientTok(req.ClientPublicKey, UserId);
string token = new PasetoBuilder().Use(ProtocolVersion.V4, Purpose.Local)
.WithKey(JsonDb.Instance.LauncherTokenKey, Encryption.SymmetricKey)
.AddClaim("userId", UserId)
.IssuedAt(DateTime.UtcNow)
.Expiration(DateTime.UtcNow.AddDays(2))
.Encode();
string encryptionToken = new PasetoBuilder().Use(ProtocolVersion.V4, Purpose.Local)
.WithKey(JsonDb.Instance.LauncherTokenKey, Encryption.SymmetricKey)
.AddClaim("data", JsonSerializer.Serialize(rsp))
.IssuedAt(DateTime.UtcNow)
.Expiration(DateTime.UtcNow.AddDays(2))
.Encode();
ResEnterServer response = new()
{
GameClientToken = token,
FeatureDataInfo = new NetFeatureDataInfo() { }, // TODO
Identifier = new NetLegacyUserIdentifier() { Server = 1000, Usn = (long)user.ID },
ShouldRestartAfter = Duration.FromTimeSpan(TimeSpan.FromSeconds(86400)),
EncryptionToken = ByteString.CopyFromUtf8(encryptionToken)
};
user.ResetDataIfNeeded();
await WriteDataAsync(response);
}
}
}

View File

@@ -2,18 +2,18 @@
using EpinelPS.Utils;
using Google.Protobuf.WellKnownTypes;
namespace EpinelPS.LobbyServer.Msgs.Auth
namespace EpinelPS.LobbyServer.Auth
{
[PacketPath("/auth/intl")]
public class DoIntlAuth : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqAuthIntl>();
var response = new ResAuth();
ReqAuthIntl req = await ReadData<ReqAuthIntl>();
ResAuth response = new();
UsedAuthToken = req.Token;
foreach (var item in JsonDb.Instance.LauncherAccessTokens)
foreach (AccessToken item in JsonDb.Instance.LauncherAccessTokens)
{
if (item.Token == UsedAuthToken)
{
@@ -22,11 +22,11 @@ namespace EpinelPS.LobbyServer.Msgs.Auth
}
if (UserId == 0)
{
response.AuthError = new NetAuthError() { ErrorCode = AuthErrorCode.AuthErrorCodeError };
response.AuthError = new NetAuthError() { ErrorCode = AuthErrorCode.Error };
}
else
{
var user = GetUser();
User user = GetUser();
if (user.IsBanned && user.BanEnd < DateTime.UtcNow)
{
@@ -43,7 +43,7 @@ namespace EpinelPS.LobbyServer.Msgs.Auth
}
else
{
response.AuthSuccess = new NetAuthSuccess() { AuthToken = req.Token, CentauriZoneId = "84", FirstAuth = false, PurchaseRestriction = new NetUserPurchaseRestriction() { PurchaseRestriction = PurchaseRestriction.PurchaseRestrictionChild, UpdatedAt = 638546758794611090 } };
response.AuthSuccess = new NetAuthSuccess() { AuthToken = req.Token, CentauriZoneId = "84", FirstAuth = false, PurchaseRestriction = new NetUserPurchaseRestriction() { PurchaseRestriction = PurchaseRestriction.Child, UpdatedAt = 638546758794611090 } };
}
}

View File

@@ -0,0 +1,26 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Badge
{
[PacketPath("/badge/delete")]
public class DeleteBadge : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqDeleteBadge req = await ReadData<ReqDeleteBadge>();
User user = GetUser();
ResDeleteBadge response = new();
foreach (long badgeId in req.BadgeSeqList)
{
user.Badges.RemoveAll(x => x.Seq == badgeId);
}
JsonDb.Save();
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

@@ -0,0 +1,24 @@
using EpinelPS.Utils;
using Google.Protobuf;
namespace EpinelPS.LobbyServer.Badge
{
[PacketPath("/badge/sync")]
public class SyncBadge : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSyncBadge req = await ReadData<ReqSyncBadge>();
User user = GetUser();
ResSyncBadge response = new();
foreach (BadgeModel item in user.Badges)
{
response.BadgeList.Add(item.ToNet());
}
await WriteDataAsync(response);
}
}
}

View File

@@ -1,15 +1,15 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Campaign
namespace EpinelPS.LobbyServer.Campaign
{
[PacketPath("/shutdownflags/campaignpackage/getall")]
public class CampaignPackageGetAll : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqCampaignPackageGetAllShutdownFlags>();
ReqCampaignPackageGetAllShutdownFlags req = await ReadData<ReqCampaignPackageGetAllShutdownFlags>();
var response = new ResCampaignPackageGetAllShutdownFlags();
ResCampaignPackageGetAllShutdownFlags response = new();
// TODO
await WriteDataAsync(response);

View File

@@ -1,27 +1,29 @@
using EpinelPS.LobbyServer.Msgs.Stage;
using EpinelPS.StaticInfo;
using EpinelPS.LobbyServer.Stage;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Campaign
namespace EpinelPS.LobbyServer.Campaign
{
[PacketPath("/campaign/getfield")]
public class GetCampaignField : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqGetCampaignFieldData>();
var user = GetUser();
ReqGetCampaignFieldData req = await ReadData<ReqGetCampaignFieldData>();
User user = GetUser();
Console.WriteLine("Map ID: " + req.MapId);
var response = new ResGetCampaignFieldData();
response.Field = GetStage.CreateFieldInfo(user, GameData.Instance.GetNormalChapterNumberFromFieldName(req.MapId), req.MapId.Contains("hard") ? "Hard" : "Normal");
ResGetCampaignFieldData response = new()
{
Field = GetStage.CreateFieldInfo(user, req.MapId, out bool bossEntered),
// todo save this data
response.Team = new NetUserTeamData() { LastContentsTeamNumber = 1, Type = 1 };
// todo save this data
Team = new NetUserTeamData() { LastContentsTeamNumber = 1, Type = 1 }
};
if (user.LastNormalStageCleared >= 6000003)
{
var team = new NetTeamData() { TeamNumber = 1 };
NetTeamData team = new() { TeamNumber = 1 };
team.Slots.Add(new NetTeamSlot() { Slot = 1, Value = 47263455 });
team.Slots.Add(new NetTeamSlot() { Slot = 2, Value = 47263456 });
team.Slots.Add(new NetTeamSlot() { Slot = 3, Value = 47263457 });
@@ -33,14 +35,14 @@ namespace EpinelPS.LobbyServer.Msgs.Campaign
}
string resultingJson;
if (!user.MapJson.ContainsKey(req.MapId))
if (!user.MapJson.TryGetValue(req.MapId, out string? value))
{
resultingJson = "";
user.MapJson.Add(req.MapId, resultingJson);
}
else
{
resultingJson = user.MapJson[req.MapId];
resultingJson = value;
}
response.Json = resultingJson;

View File

@@ -0,0 +1,30 @@
using EpinelPS.Database;
using EpinelPS.LobbyServer.Stage;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Campaign
{
[PacketPath("/campaign/getfieldobjectitemsnum")]
public class GetFieldObjectsCount : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetCampaignFieldObjectItemsNum req = await ReadData<ReqGetCampaignFieldObjectItemsNum>();
User user = GetUser();
ResGetCampaignFieldObjectItemsNum response = new();
foreach (KeyValuePair<string, FieldInfoNew> map in user.FieldInfoNew)
{
response.FieldObjectItemsNum.Add(new NetCampaignFieldObjectItemsNum()
{
MapId = map.Key,
Count = map.Value.CompletedObjects.Where(x => x.Type == 1).Count()
});
}
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,52 @@
using EpinelPS.Database;
using EpinelPS.LobbyServer.Stage;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Campaign
{
[PacketPath("/campaign/obtain/item")]
public class ObtainItem : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqObtainCampaignItem req = await ReadData<ReqObtainCampaignItem>();
User user = GetUser();
ResObtainCampaignItem response = new();
if (!user.FieldInfoNew.TryGetValue(req.MapId, out FieldInfoNew? field))
{
field = new FieldInfoNew();
user.FieldInfoNew.Add(req.MapId, field);
}
foreach (NetFieldObject item in field.CompletedObjects)
{
if (item.PositionId == req.FieldObject.PositionId)
{
Logging.WriteLine("attempted to collect campaign field object twice!", LogType.WarningAntiCheat);
return;
}
}
// Register and return reward
var map = GameData.Instance.MapData[req.MapId];
var position = map.ItemSpawner.Where(x => x.PositionId == req.FieldObject.PositionId).FirstOrDefault() ?? throw new Exception("bad position Id");
FieldItemRecord positionReward = GameData.Instance.FieldItems[position.ItemId];
RewardRecord reward = GameData.Instance.GetRewardTableEntry(positionReward.TypeValue) ?? throw new Exception("failed to get reward");
response.Reward = RewardUtils.RegisterRewardsForUser(user, reward);
// HIde it from the field
field.CompletedObjects.Add(new NetFieldObject() { PositionId = req.FieldObject.PositionId, Type = req.FieldObject.Type});
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -1,18 +1,16 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Campaign
namespace EpinelPS.LobbyServer.Campaign
{
[PacketPath("/campaign/savefield")]
public class SaveField : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSaveCampaignField>();
var user = GetUser();
ReqSaveCampaignField req = await ReadData<ReqSaveCampaignField>();
User user = GetUser();
var response = new ResGetFieldTalkList();
Console.WriteLine($"save {req.MapId} with {req.Json}");
ResSaveCampaignField response = new();
if (!user.MapJson.ContainsKey(req.MapId))
{

View File

@@ -0,0 +1,27 @@
using EpinelPS.Database;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Campaign
{
[PacketPath("/campaign/savefieldobject")]
public class SaveFieldObject : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSaveCampaignFieldObject req = await ReadData<ReqSaveCampaignFieldObject>();
User user = GetUser();
ResSaveCampaignFieldObject response = new();
Logging.WriteLine($"save {req.MapId} with {req.FieldObject.PositionId}", LogType.Debug);
FieldInfoNew field = user.FieldInfoNew[req.MapId];
field.CompletedObjects.Add(new NetFieldObject() { PositionId = req.FieldObject.PositionId, Json = req.FieldObject.Json, Type = req.FieldObject.Type });
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -1,37 +1,38 @@
using EpinelPS.Utils;
using EpinelPS.Database;
using EpinelPS.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/SynchroDevice/Change")]
public class ChangeSynchroDevice : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSynchroChange>();
var user = GetUser();
ReqSynchroChange req = await ReadData<ReqSynchroChange>();
User user = GetUser();
var response = new ResSynchroChange();
ResSynchroChange response = new();
var highestLevelCharacters = user.Characters.OrderByDescending(x => x.Level).Take(5).ToList();
List<CharacterModel> highestLevelCharacters = [.. user.Characters.OrderByDescending(x => x.Level).Take(5)];
int slot = 1;
foreach (var item in highestLevelCharacters)
foreach (CharacterModel? item in highestLevelCharacters)
{
if (item.Level != 200)
{
throw new Exception("expected level to be 200");
}
response.Characters.Add(new NetUserCharacterData() { Default = new() { Csn = item.Csn, Skill1Lv = item.Skill1Lvl, Skill2Lv = item.Skill2Lvl, CostumeId = item.CostumeId, Level = item.Level, Grade = item.Grade, Tid = item.Tid, UltiSkillLv = item.UltimateLevel }, IsSynchro = user.GetSynchro(item.Csn) });
response.Characters.Add(new NetUserCharacterData() { Default = new() { Csn = item.Csn, Skill1Lv = item.Skill1Lvl, Skill2Lv = item.Skill2Lvl, CostumeId = item.CostumeId, Lv = item.Level, Grade = item.Grade, Tid = item.Tid, UltiSkillLv = item.UltimateLevel }, IsSynchro = user.GetSynchro(item.Csn) });
foreach (var s in user.SynchroSlots)
foreach (SynchroSlot s in user.SynchroSlots)
{
if (s.Slot == slot)
{
@@ -44,10 +45,13 @@ namespace EpinelPS.LobbyServer.Msgs.Character
user.SynchroDeviceUpgraded = true;
foreach (var item in user.SynchroSlots)
foreach (SynchroSlot item in user.SynchroSlots)
{
response.Slots.Add(new NetSynchroSlot() { Slot = item.Slot, AvailableRegisterAt = item.AvailableAt, Csn = item.CharacterSerialNumber });
}
JsonDb.Save();
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,18 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character.Counsel;
[PacketPath("/character/attractive/check")]
public class CheckCharacterCounsel : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCounseledBefore req = await ReadData<ReqCounseledBefore>();
User user = GetUser();
ResCounseledBefore response = new();
// TODO: Validate response from real server and pull info from user info
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,18 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character.Counsel;
[PacketPath("/character/counsel/check")]
public class CheckCounsel : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCounseledBefore req = await ReadData<ReqCounseledBefore>();
ResCounseledBefore response = new();
response.IsCounseledBefore = false;
await WriteDataAsync(response);
}
}

View File

@@ -0,0 +1,99 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character.Counsel
{
[PacketPath("/character/attractive/counsel")]
public class DoCounsel : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCharacterCounsel req = await ReadData<ReqCharacterCounsel>();
User user = GetUser();
ResCharacterCounsel response = new();
foreach (KeyValuePair<CurrencyType, long> currency in user.Currency)
{
response.Currencies.Add(new NetUserCurrencyData() { Type = (int)currency.Key, Value = currency.Value });
}
NetUserAttractiveData? currentBondInfo = user.BondInfo.FirstOrDefault(x => x.NameCode == req.NameCode);
if (currentBondInfo != null)
{
int beforeLv = currentBondInfo.Lv;
int beforeExp = currentBondInfo.Exp;
currentBondInfo.Exp += 100;
currentBondInfo.CounseledCount++;
currentBondInfo.CanCounselToday = true; // Always allow counseling
UpdateAttractiveLevel(currentBondInfo);
response.Attractive = currentBondInfo;
response.Exp = new NetIncreaseExpData
{
NameCode = currentBondInfo.NameCode,
BeforeLv = beforeLv,
BeforeExp = beforeExp,
CurrentLv = currentBondInfo.Lv,
CurrentExp = currentBondInfo.Exp,
GainExp = 100
};
}
else
{
NetUserAttractiveData data = new NetUserAttractiveData()
{
NameCode = req.NameCode,
Exp = 100,
CounseledCount = 1,
IsFavorites = false,
CanCounselToday = true,
Lv = 1
};
UpdateAttractiveLevel(data);
user.BondInfo.Add(data);
response.Attractive = data;
response.Exp = new NetIncreaseExpData
{
NameCode = data.NameCode,
BeforeLv = 1,
BeforeExp = 0,
CurrentLv = 1,
CurrentExp = 100,
GainExp = 100
};
}
JsonDb.Save();
await WriteDataAsync(response);
}
private void UpdateAttractiveLevel(NetUserAttractiveData attractiveData)
{
while (attractiveData.Lv < 40)
{
AttractiveLevelRecord? levelInfo = GameData.Instance.AttractiveLevelTable.FirstOrDefault(x => x.Value.AttractiveLevel == attractiveData.Lv).Value;
if (levelInfo == null)
{
// No more level data
break;
}
if (attractiveData.Exp >= levelInfo.AttractivePoint)
{
attractiveData.Exp -= levelInfo.AttractivePoint;
attractiveData.Lv++;
}
else
{
break;
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character.Counsel
{
[PacketPath("/character/attractive/present")]
public class Present : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCharacterPresent req = await ReadData<ReqCharacterPresent>();
User user = GetUser();
ResCharacterPresent response = new ResCharacterPresent();
NetUserAttractiveData? bondInfo = user.BondInfo.FirstOrDefault(x => x.NameCode == req.NameCode);
if (bondInfo == null)
{
return;
}
int totalExpGained = 0;
CharacterRecord? characterRecord = GameData.Instance.CharacterTable.Values.FirstOrDefault(x => x.NameCode == req.NameCode);
foreach (NetItemData item in req.Items)
{
ItemMaterialRecord? materialInfo = GameData.Instance.itemMaterialTable.GetValueOrDefault(item.Tid);
if (materialInfo != null && materialInfo.ItemSubType == ItemSubType.AttractiveMaterial)
{
int expGained = materialInfo.ItemValue * (int)item.Count;
if (characterRecord != null)
{
if (materialInfo.MaterialType == MaterialType.Corporation)
{
string corporation = materialInfo.NameLocalkey.Split('_')[2];
if (corporation.Equals(characterRecord.Corporation.ToString(), StringComparison.OrdinalIgnoreCase))
{
expGained *= 5;
}
}
else if (materialInfo.MaterialType == MaterialType.Squad)
{
string squad = materialInfo.NameLocalkey.Split('_')[2];
if (squad.Equals(characterRecord.Squad.ToString(), StringComparison.OrdinalIgnoreCase))
{
expGained *= 3;
}
}
}
totalExpGained += expGained;
ItemData? userItem = user.Items.FirstOrDefault(x => x.ItemType == item.Tid);
if (userItem != null)
{
userItem.Count -= (int)item.Count;
if (userItem.Count <= 0)
{
user.Items.Remove(userItem);
}
}
}
}
int beforeLv = bondInfo.Lv;
int beforeExp = bondInfo.Exp;
bondInfo.Exp += totalExpGained;
UpdateAttractiveLevel(bondInfo);
response.Attractive = bondInfo;
response.Exp = new NetIncreaseExpData
{
NameCode = bondInfo.NameCode,
BeforeLv = beforeLv,
BeforeExp = beforeExp,
CurrentLv = bondInfo.Lv,
CurrentExp = bondInfo.Exp,
GainExp = totalExpGained
};
response.Items.AddRange(NetUtils.GetUserItems(user));
JsonDb.Save();
await WriteDataAsync(response);
}
private void UpdateAttractiveLevel(NetUserAttractiveData attractiveData)
{
while (attractiveData.Lv < 40)
{
AttractiveLevelRecord? levelInfo = GameData.Instance.AttractiveLevelTable.Values.FirstOrDefault(x => x.AttractiveLevel == attractiveData.Lv);
if (levelInfo == null)
{
break;
}
if (attractiveData.Exp >= levelInfo.AttractivePoint)
{
attractiveData.Exp -= levelInfo.AttractivePoint;
attractiveData.Lv++;
}
else
{
break;
}
}
}
}
}

View File

@@ -0,0 +1,99 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character.Counsel
{
[PacketPath("/character/counsel/quick")]
public class QuickCounsel : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCharacterQuickCounsel req = await ReadData<ReqCharacterQuickCounsel>();
User user = GetUser();
ResCharacterQuickCounsel response = new ResCharacterQuickCounsel();
foreach (KeyValuePair<CurrencyType, long> currency in user.Currency)
{
response.Currencies.Add(new NetUserCurrencyData() { Type = (int)currency.Key, Value = currency.Value });
}
NetUserAttractiveData? bondInfo = user.BondInfo.FirstOrDefault(x => x.NameCode == req.NameCode);
if (bondInfo != null)
{
int beforeLv = bondInfo.Lv;
int beforeExp = bondInfo.Exp;
bondInfo.Exp += 100;
bondInfo.CounseledCount++;
bondInfo.CanCounselToday = true; // Always allow counseling
UpdateAttractiveLevel(bondInfo);
response.Attractive = bondInfo;
response.Exp = new NetIncreaseExpData
{
NameCode = bondInfo.NameCode,
BeforeLv = beforeLv,
BeforeExp = beforeExp,
CurrentLv = bondInfo.Lv,
CurrentExp = bondInfo.Exp,
GainExp = 100
};
}
else
{
NetUserAttractiveData data = new NetUserAttractiveData()
{
NameCode = req.NameCode,
Exp = 100,
CounseledCount = 1,
IsFavorites = false,
CanCounselToday = true,
Lv = 1
};
UpdateAttractiveLevel(data);
user.BondInfo.Add(data);
response.Attractive = data;
response.Exp = new NetIncreaseExpData
{
NameCode = data.NameCode,
BeforeLv = 1,
BeforeExp = 0,
CurrentLv = 1,
CurrentExp = 100,
GainExp = 100
};
}
JsonDb.Save();
await WriteDataAsync(response);
}
private void UpdateAttractiveLevel(NetUserAttractiveData attractiveData)
{
while (attractiveData.Lv < 40)
{
AttractiveLevelRecord? levelInfo = GameData.Instance.AttractiveLevelTable.Values.FirstOrDefault(x => x.AttractiveLevel == attractiveData.Lv);
if (levelInfo == null)
{
// No more level data
break;
}
if (attractiveData.Exp >= levelInfo.AttractivePoint)
{
attractiveData.Exp -= levelInfo.AttractivePoint;
attractiveData.Lv++;
}
else
{
break;
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
using EpinelPS.Database;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/upgrade")]
public class DoLimitBreak : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// Read the incoming request that contains the current CSN and ISN
ReqCharacterUpgrade req = await ReadData<ReqCharacterUpgrade>(); // Contains csn and isn (read-only)
ResCharacterUpgrade response = new();
User user = GetUser();
// Get all character data from the game's character table
List<CharacterRecord> fullchardata = [.. GameData.Instance.CharacterTable.Values];
CharacterModel targetCharacter = user.GetCharacterBySerialNumber(req.Csn) ?? throw new NullReferenceException();
// Find the element with the current csn from the request
CharacterRecord currentCharacter = fullchardata.FirstOrDefault(c => c.Id == targetCharacter.Tid) ?? throw new NullReferenceException();
if (currentCharacter != null && targetCharacter != null)
{
if (currentCharacter.GradeCoreId == 103 || currentCharacter.GradeCoreId == 11 || currentCharacter.GradeCoreId == 201)
{
Console.WriteLine("cannot limit break any further!");
await WriteDataAsync(response);
return;
}
// Find a new CSN based on the `NameCode` of the current character and `GradeCoreId + 1`
// For some reason, there is a seperate character for each limit/core break value.
CharacterRecord? newCharacter = fullchardata.FirstOrDefault(c => c.NameCode == currentCharacter.NameCode && c.GradeCoreId == currentCharacter.GradeCoreId + 1);
if (newCharacter != null)
{
// replace character in DB with new character
targetCharacter.Grade++;
targetCharacter.Tid = newCharacter.Id;
response.Character = new NetUserCharacterDefaultData()
{
Csn = req.Csn,
CostumeId = targetCharacter.CostumeId,
Grade = targetCharacter.Grade,
Lv = user.GetSynchroLevel(),
Skill1Lv = targetCharacter.Skill1Lvl,
Skill2Lv = targetCharacter.Skill2Lvl,
Tid = targetCharacter.Tid,
UltiSkillLv = targetCharacter.UltimateLevel
};
// remove spare body item
ItemData bodyItem = user.Items.FirstOrDefault(i => i.Isn == req.Isn) ?? throw new NullReferenceException();
user.RemoveItemBySerialNumber(req.Isn, 1);
response.Items.Add(NetUtils.ToNet(bodyItem));
JsonDb.Save();
}
}
// Send the response back to the client
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,29 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/attractive/get")]
public class GetCharacterAttractiveList : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetAttractiveList req = await ReadData<ReqGetAttractiveList>();
User user = GetUser();
ResGetAttractiveList response = new()
{
CounselAvailableCount = 3 // TODO
};
foreach (NetUserAttractiveData item in user.BondInfo)
{
response.Attractives.Add(item);
item.CanCounselToday = true;
}
// TODO: Validate response from real server and pull info from user info
await WriteDataAsync(response);
}
}
}

View File

@@ -1,16 +1,16 @@
using EpinelPS.StaticInfo;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/costume/get")]
public class GetCharacterCostume : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqGetCharacterCostumeData>();
ReqGetCharacterCostumeData req = await ReadData<ReqGetCharacterCostumeData>();
var response = new ResGetCharacterCostumeData();
ResGetCharacterCostumeData response = new();
// return all
response.CostumeIds.AddRange(GameData.Instance.GetAllCostumes());

View File

@@ -0,0 +1,49 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/get")]
public class GetCharacterData : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetCharacterData req = await ReadData<ReqGetCharacterData>();
User user = GetUser();
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)
});
// 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)];
foreach (CharacterModel? c in highestLevelCharacters)
{
response.SynchroStandardCharacters.Add(c.Csn);
}
await WriteDataAsync(response);
}
}
}

View File

@@ -1,45 +1,41 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/synchrodevice/get")]
public class GetSynchrodevice : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqGetSynchroData>();
var user = GetUser();
ReqGetSynchroData req = await ReadData<ReqGetSynchroData>();
User user = GetUser();
if (user.SynchroSlots.Count == 0)
{
user.SynchroSlots = new() {
user.SynchroSlots = [
new SynchroSlot() { Slot = 1 },
new SynchroSlot() { Slot = 2},
new SynchroSlot() { Slot = 3 },
new SynchroSlot() { Slot = 4 },
new SynchroSlot() { Slot = 5 },
new SynchroSlot() { Slot = 6 },
new SynchroSlot() { Slot = 7 },
new SynchroSlot() { Slot = 8 },
new SynchroSlot() { Slot = 9 },
new SynchroSlot() { Slot = 10 },
};
];
}
var highestLevelCharacters = user.Characters.OrderByDescending(x => x.Level).Take(5).ToList();
List<CharacterModel> highestLevelCharacters = [.. user.Characters.OrderByDescending(x => x.Level).Take(5)];
var response = new ResGetSynchroData();
response.Synchro = new NetUserSynchroData();
foreach (var item in highestLevelCharacters)
ResGetSynchroData response = new()
{
response.Synchro.StandardCharacters.Add(new NetUserCharacterData() { Default = new() { Csn = item.Csn, Skill1Lv = item.Skill1Lvl, Skill2Lv = item.Skill2Lvl, CostumeId = item.CostumeId, Level = item.Level, Grade = item.Grade, Tid = item.Tid, UltiSkillLv = item.UltimateLevel }, IsSynchro = user.GetSynchro(item.Csn) });
Synchro = new NetUserSynchroData()
};
foreach (CharacterModel? item in highestLevelCharacters)
{
response.Synchro.StandardCharacters.Add(new NetUserCharacterData() { Default = new() { Csn = item.Csn, Skill1Lv = item.Skill1Lvl, Skill2Lv = item.Skill2Lvl, CostumeId = item.CostumeId, Lv = item.Level, Grade = item.Grade, Tid = item.Tid, UltiSkillLv = item.UltimateLevel }, IsSynchro = user.GetSynchro(item.Csn) });
}
foreach (var item in user.SynchroSlots)
foreach (SynchroSlot item in user.SynchroSlots)
{
response.Synchro.Slots.Add(new NetSynchroSlot() { Slot = item.Slot, AvailableRegisterAt = 1, Csn = item.CharacterSerialNumber });
}

View File

@@ -1,32 +1,32 @@
using EpinelPS.Database;
using EpinelPS.StaticInfo;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/levelup")]
public class LevelUp : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqCharacterLevelUp>();
var user = GetUser();
var response = new ResCharacterLevelUp();
var data = GameData.Instance.GetCharacterLevelUpData();
ReqCharacterLevelUp req = await ReadData<ReqCharacterLevelUp>();
User user = GetUser();
ResCharacterLevelUp response = new();
Dictionary<int, CharacterLevelRecord> data = GameData.Instance.GetCharacterLevelUpData();
foreach (var item in user.Characters.ToArray())
foreach (CharacterModel item in user.Characters.ToArray())
{
if (item.Csn == req.Csn)
{
int requiredCredit = 0;
int requiredBattleData = 0;
int requiredCoreDust = 0;
for (int i = item.Level; i < req.Level; i++)
for (int i = item.Level; i < req.Lv; i++)
{
var levelUpData = data[i];
requiredCredit += levelUpData.gold;
requiredBattleData += levelUpData.character_exp;
requiredCoreDust += levelUpData.character_exp2;
CharacterLevelRecord levelUpData = data[i];
requiredCredit += levelUpData.Gold;
requiredBattleData += levelUpData.CharacterExp;
requiredCoreDust += levelUpData.CharacterExp2;
}
if (user.CanSubtractCurrency(CurrencyType.Gold, requiredCredit) &&
@@ -36,12 +36,11 @@ namespace EpinelPS.LobbyServer.Msgs.Character
user.SubtractCurrency(CurrencyType.Gold, requiredCredit);
user.SubtractCurrency(CurrencyType.CharacterExp, requiredBattleData);
user.SubtractCurrency(CurrencyType.CharacterExp2, requiredCoreDust);
item.Level = req.Level;
item.Level = req.Lv;
}
else
{
// TOOD: log this
Console.WriteLine("ERROR: Not enough currency for upgrade");
Logging.WriteLine("ERROR: Not enough currency for upgrade", LogType.WarningAntiCheat);
return;
}
@@ -49,23 +48,23 @@ namespace EpinelPS.LobbyServer.Msgs.Character
{
CostumeId = item.CostumeId,
Csn = item.Csn,
Level = item.Level,
Lv = item.Level,
Skill1Lv = item.Skill1Lvl,
Skill2Lv = item.Skill2Lvl,
UltiSkillLv = item.UltimateLevel,
Grade = item.Grade,
Tid = item.Tid
};
var highestLevelCharacters = user.Characters.OrderByDescending(x => x.Level).Take(5).ToList();
List<CharacterModel> highestLevelCharacters = [.. user.Characters.OrderByDescending(x => x.Level).Take(5)];
response.SynchroLv = user.GetSynchroLevel();
foreach (var c in highestLevelCharacters)
foreach (CharacterModel? c in highestLevelCharacters)
{
response.SynchroStandardCharacters.Add(c.Csn);
}
foreach (var currency in user.Currency)
foreach (KeyValuePair<CurrencyType, long> currency in user.Currency)
{
response.Currencies.Add(new NetUserCurrencyData() { Type = (int)currency.Key, Value = currency.Value });
}
@@ -73,6 +72,8 @@ namespace EpinelPS.LobbyServer.Msgs.Character
break;
}
}
user.AddTrigger(Trigger.CharacterLevelUpCount, 1);
JsonDb.Save();
await WriteDataAsync(response);

View File

@@ -0,0 +1,39 @@
using EpinelPS.Utils;
using EpinelPS.Data;
using EpinelPS.Database;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/attractive/obtainreward")]
public class ObtainEpReward : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqObtainAttractiveReward req = await ReadData<ReqObtainAttractiveReward>();
ResObtainAttractiveReward response = new();
User user = GetUser();
// look up ID from name code and level
KeyValuePair<int, AttractiveLevelRewardRecord> levelUpRecord = GameData.Instance.AttractiveLevelReward.Where(x => x.Value.AttractiveLevel == req.Lv && x.Value.NameCode == req.NameCode).FirstOrDefault();
foreach (NetUserAttractiveData item in user.BondInfo)
{
if (item.NameCode == req.NameCode)
{
if (!item.ObtainedRewardLevels.Contains(levelUpRecord.Value.Id))
{
item.ObtainedRewardLevels.Add(levelUpRecord.Value.Id);
RewardRecord reward = GameData.Instance.GetRewardTableEntry(levelUpRecord.Value.RewardId) ?? throw new Exception("failed to get reward");
response.Reward = RewardUtils.RegisterRewardsForUser(user, reward);
JsonDb.Save();
}
break;
}
}
await WriteDataAsync(response);
}
}
}

View File

@@ -1,19 +1,18 @@
using EpinelPS.Utils;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/SynchroDevice/Regist")]
public class RegisterSynchroDevice : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSynchroRegister>();
var user = GetUser();
var targetCharacter = user.GetCharacterBySerialNumber(req.Csn);
if (targetCharacter == null) throw new Exception("target character does not exist");
var response = new ResSynchroRegister();
foreach (var item in user.SynchroSlots)
ReqSynchroRegister req = await ReadData<ReqSynchroRegister>();
User user = GetUser();
CharacterModel? targetCharacter = user.GetCharacterBySerialNumber(req.Csn) ?? throw new Exception("target character does not exist");
ResSynchroRegister response = new();
foreach (SynchroSlot item in user.SynchroSlots)
{
if (item.Slot == req.Slot)
{
@@ -30,7 +29,7 @@ namespace EpinelPS.LobbyServer.Msgs.Character
Csn = item.CharacterSerialNumber,
CostumeId = targetCharacter.CostumeId,
Grade = targetCharacter.Grade,
Level = user.GetSynchroLevel(),
Lv = user.GetSynchroLevel(),
Skill1Lv = targetCharacter.Skill1Lvl,
Skill2Lv = targetCharacter.Skill2Lvl,
Tid = targetCharacter.Tid,
@@ -41,6 +40,7 @@ namespace EpinelPS.LobbyServer.Msgs.Character
}
}
JsonDb.Save();
await WriteDataAsync(response);
}

View File

@@ -1,31 +1,31 @@
using EpinelPS.Database;
using EpinelPS.StaticInfo;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/growreset")]
public class ResetLevel : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqCharacterGrowReset>();
var user = GetUser();
var response = new ResCharacterGrowReset();
var data = GameData.Instance.GetCharacterLevelUpData();
ReqCharacterGrowReset req = await ReadData<ReqCharacterGrowReset>();
User user = GetUser();
ResCharacterGrowReset response = new();
Dictionary<int, CharacterLevelRecord> data = GameData.Instance.GetCharacterLevelUpData();
foreach (var item in user.Characters.ToArray())
foreach (CharacterModel item in user.Characters.ToArray())
{
if (item.Csn == req.Csn)
{
if (item.Level == 1)
{
Console.WriteLine("Character level is already 1 - cannot reset");
Logging.WriteLine("Character level is already 1 - cannot reset", LogType.WarningAntiCheat);
return;
}
if (item.Level == 200)
{
Console.WriteLine("Character level is 200 - cannot reset");
Logging.WriteLine("Character level is 200 - cannot reset", LogType.WarningAntiCheat);
return;
}
@@ -34,10 +34,10 @@ namespace EpinelPS.LobbyServer.Msgs.Character
int requiredCoreDust = 0;
for (int i = 1; i < item.Level; i++)
{
var levelUpData = data[i];
requiredCredit += levelUpData.gold;
requiredBattleData += levelUpData.character_exp;
requiredCoreDust += levelUpData.character_exp2;
CharacterLevelRecord levelUpData = data[i];
requiredCredit += levelUpData.Gold;
requiredBattleData += levelUpData.CharacterExp;
requiredCoreDust += levelUpData.CharacterExp2;
}
user.AddCurrency(CurrencyType.Gold, requiredCredit);
@@ -49,23 +49,23 @@ namespace EpinelPS.LobbyServer.Msgs.Character
{
CostumeId = item.CostumeId,
Csn = item.Csn,
Level = item.Level,
Lv = item.Level,
Skill1Lv = item.Skill1Lvl,
Skill2Lv = item.Skill2Lvl,
UltiSkillLv = item.UltimateLevel,
Grade = item.Grade,
Tid = item.Tid
};
var highestLevelCharacters = user.Characters.OrderByDescending(x => x.Level).Take(5).ToList();
List<CharacterModel> highestLevelCharacters = [.. user.Characters.OrderByDescending(x => x.Level).Take(5)];
response.SynchroLv = highestLevelCharacters.Last().Level;
foreach (var c in highestLevelCharacters)
foreach (CharacterModel? c in highestLevelCharacters)
{
response.SynchroStandardCharacters.Add(c.Csn);
}
foreach (var currency in user.Currency)
foreach (KeyValuePair<CurrencyType, long> currency in user.Currency)
{
response.Currencies.Add(new NetUserCurrencyData() { Type = (int)currency.Key, Value = currency.Value });
}

View File

@@ -1,17 +1,17 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/costume/set")]
public class SetCharacterCostume : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSetCharacterCostume>();
var user = GetUser();
ReqSetCharacterCostume req = await ReadData<ReqSetCharacterCostume>();
User user = GetUser();
foreach (var item in user.Characters)
foreach (CharacterModel item in user.Characters)
{
if (item.Csn == req.Csn)
{
@@ -21,7 +21,7 @@ namespace EpinelPS.LobbyServer.Msgs.Character
}
JsonDb.Save();
var response = new ResSetCharacterCostume();
ResSetCharacterCostume response = new();
await WriteDataAsync(response);
}

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

@@ -0,0 +1,94 @@
using EpinelPS.Database;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/skill/levelup")]
public class SkillLevelUp : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCharacterSkillLevelUp req = await ReadData<ReqCharacterSkillLevelUp>();
User user = GetUser();
ResCharacterSkillLevelUp response = new();
CharacterModel character = user.Characters.FirstOrDefault(c => c.Csn == req.Csn) ?? throw new Exception("cannot find character");
CharacterRecord charRecord = GameData.Instance.CharacterTable.Values.FirstOrDefault(c => c.Id == character.Tid) ?? throw new Exception("cannot find character record");
Dictionary<int, int> skillIdMap = new()
{
{ 1, charRecord.UltiSkillId },
{ 2, charRecord.Skill1Id },
{ 3, charRecord.Skill2Id }
};
Dictionary<int, int> skillLevelMap = new()
{
{ 1, character.UltimateLevel },
{ 2, character.Skill1Lvl },
{ 3, character.Skill2Lvl }
};
SkillInfoRecord skillRecord = GameData.Instance.skillInfoTable.Values.FirstOrDefault(s => s.Id == skillIdMap[req.Category] + (skillLevelMap[req.Category] - 1)) ?? throw new Exception("cannot find character skill record");
CostRecord costRecord = GameData.Instance.costTable.Values.FirstOrDefault(c => c.Id == skillRecord.LevelUpCostId) ?? throw new Exception("cannot find character cost record");
foreach (CostData? cost in costRecord.Costs.Where(i => i.ItemType != RewardType.None))
{
ItemData item = user.Items.FirstOrDefault(i => i.ItemType == cost.ItemId) ?? throw new NullReferenceException();
item.Count -= cost.ItemValue;
response.Items.Add(new NetUserItemData
{
Isn = item.Isn,
Tid = cost.ItemId,
Count = item.Count,
Csn = item.Csn,
Corporation = item.Corp,
Lv = item.Level,
Exp = item.Exp,
Position = item.Position
});
}
NetUserCharacterDefaultData newChar = new()
{
CostumeId = character.CostumeId,
Csn = character.Csn,
Lv = character.Level,
Grade = character.Grade,
Tid = character.Tid,
DispatchTid = character.Tid,
Skill1Lv = character.Skill1Lvl,
Skill2Lv = character.Skill2Lvl,
UltiSkillLv = character.UltimateLevel,
};
if (req.Category == 1)
{
character.UltimateLevel++;
newChar.UltiSkillLv++;
}
else if (req.Category == 2)
{
character.Skill1Lvl++;
newChar.Skill1Lv++;
}
else if (req.Category == 3)
{
character.Skill2Lvl++;
newChar.Skill2Lv++;
}
if (character.UltimateLevel == 10 && character.Skill1Lvl == 10 && character.Skill2Lvl == 10)
{
user.AddTrigger(Trigger.CharacterSkillLevelMax, 1);
}
response.Character = newChar;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,49 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/synchrodevice/addslot")]
public class SynchroAddSlot : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSynchroAddSlot req = await ReadData<ReqSynchroAddSlot>();
User user = GetUser();
ResSynchroAddSlot response = new();
SynchroSlot? slot = user.SynchroSlots.FirstOrDefault(x => x.Slot == req.Slot);
if (slot != null)
{
response.Slot = new NetSynchroSlot
{
Csn = slot.CharacterSerialNumber,
Slot = slot.Slot,
AvailableRegisterAt = slot.AvailableAt
};
}
else
{
NetSynchroSlot newSlot = new()
{
Csn = 0,
Slot = req.Slot,
AvailableRegisterAt = 0
};
user.SynchroSlots.Add(new SynchroSlot()
{
Slot = newSlot.Slot,
CharacterSerialNumber = newSlot.Csn,
AvailableAt = newSlot.AvailableRegisterAt
});
response.Slot = newSlot;
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,39 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/synchrodevice/addslotbyitem")]
public class SynchroAddSlotByItem : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// Broken protocol so we dIdn't validate request data.
// May fix later.
ReqSynchroAddSlot req = await ReadData<ReqSynchroAddSlot>();
User user = GetUser();
ResSynchroAddSlot response = new();
NetSynchroSlot newSlot = new()
{
Csn = 0,
Slot = user.SynchroSlots.Last().Slot + 1, // any upper bound?
AvailableRegisterAt = 0
};
user.SynchroSlots.Add(new SynchroSlot()
{
Slot = newSlot.Slot,
CharacterSerialNumber = newSlot.Csn,
AvailableAt = newSlot.AvailableRegisterAt
});
response.Slot = newSlot;
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -1,28 +1,28 @@
using EpinelPS.Database;
using EpinelPS.StaticInfo;
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/SynchroDevice/LevelUp")]
public class SynchroLevelUp : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSynchroLevelUp>();
var user = GetUser();
ReqSynchroLevelUp req = await ReadData<ReqSynchroLevelUp>();
User user = GetUser();
var response = new ResSynchroLevelUp();
var data = GameData.Instance.GetCharacterLevelUpData();
ResSynchroLevelUp response = new();
Dictionary<int, CharacterLevelRecord> data = GameData.Instance.GetCharacterLevelUpData();
int requiredCredit = 0;
int requiredBattleData = 0;
int requiredCoreDust = 0;
var levelUpData = data[user.SynchroDeviceLevel + 1];
requiredCredit += levelUpData.gold;
requiredBattleData += levelUpData.character_exp;
requiredCoreDust += levelUpData.character_exp2;
CharacterLevelRecord levelUpData = data[user.SynchroDeviceLevel + 1];
requiredCredit += levelUpData.Gold;
requiredBattleData += levelUpData.CharacterExp;
requiredCoreDust += levelUpData.CharacterExp2;
if (user.CanSubtractCurrency(CurrencyType.Gold, requiredCredit) &&
user.CanSubtractCurrency(CurrencyType.CharacterExp, requiredBattleData) &&
@@ -41,12 +41,14 @@ namespace EpinelPS.LobbyServer.Msgs.Character
}
foreach (var currency in user.Currency)
foreach (KeyValuePair<CurrencyType, long> currency in user.Currency)
{
response.Currencies.Add(new NetUserCurrencyData() { Type = (int)currency.Key, Value = currency.Value });
}
response.SynchroLv = user.SynchroDeviceLevel;
user.AddTrigger(Trigger.CharacterLevelUpCount, 1);
JsonDb.Save();
await WriteDataAsync(response);
}

View File

@@ -1,39 +1,38 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Msgs.Character
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/SynchroDevice/Unregist")]
public class UnregisterSynchroDevice : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
var req = await ReadData<ReqSynchroUnregist>();
var user = GetUser();
ReqSynchroUnregist req = await ReadData<ReqSynchroUnregist>();
User user = GetUser();
var response = new ResSynchroUnregist();
ResSynchroUnregist response = new();
foreach (var item in user.SynchroSlots)
foreach (SynchroSlot item in user.SynchroSlots)
{
if (item.Slot == req.Slot)
{
if (item.CharacterSerialNumber == 0)
{
Console.WriteLine("must add character from synchrodevice first");
Logging.WriteLine("must add character from synchrodevice first", LogType.Warning);
}
else
{
var oldCSN = item.CharacterSerialNumber;
long oldCSN = item.CharacterSerialNumber;
item.CharacterSerialNumber = 0;
var data = user.GetCharacterBySerialNumber(oldCSN);
if (data == null) throw new Exception("failed to lookup character");
CharacterModel data = user.GetCharacterBySerialNumber(oldCSN) ?? throw new Exception("failed to lookup character");
response.Character = new NetUserCharacterDefaultData()
{
Csn = data.Csn,
CostumeId = data.CostumeId,
Grade = data.Grade,
Level = data.Level,
Lv = data.Level,
Skill1Lv = data.Skill1Lvl,
Skill2Lv = data.Skill2Lvl,
Tid = data.Tid,
@@ -42,10 +41,10 @@ namespace EpinelPS.LobbyServer.Msgs.Character
response.Slot = new NetSynchroSlot() { AvailableRegisterAt = item.AvailableAt, Csn = item.CharacterSerialNumber, Slot = item.Slot };
response.IsSynchro = false;
var highestLevelCharacters = user.Characters.OrderByDescending(x => x.Level).Take(5).ToList();
List<CharacterModel> highestLevelCharacters = [.. user.Characters.OrderByDescending(x => x.Level).Take(5)];
foreach (var item2 in highestLevelCharacters)
foreach (CharacterModel? item2 in highestLevelCharacters)
{
response.SynchroStandardCharacters.Add(item2.Csn);
}

View File

@@ -0,0 +1,76 @@
using EpinelPS.Database;
using EpinelPS.Utils;
using EpinelPS.Data;
namespace EpinelPS.LobbyServer.Character
{
[PacketPath("/character/coreupgrade")]
public class CoreUpgrade : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
// Read the incoming request that contains the current CSN and ISN
ReqCharacterCoreUpgrade req = await ReadData<ReqCharacterCoreUpgrade>(); // Contains csn and isn (read-only)
ResCharacterCoreUpgrade response = new();
User user = GetUser();
// Get all character data from the game's character table
List<CharacterRecord> fullchardata = [.. GameData.Instance.CharacterTable.Values];
CharacterModel targetCharacter = user.GetCharacterBySerialNumber(req.Csn) ?? throw new NullReferenceException();
// Find the element with the current csn from the request
CharacterRecord? currentCharacter = fullchardata.FirstOrDefault(c => c.Id == targetCharacter.Tid);
if (currentCharacter != null && targetCharacter != null)
{
if (currentCharacter.GradeCoreId == 103 || currentCharacter.GradeCoreId == 11 || currentCharacter.GradeCoreId == 201)
{
Console.WriteLine("warning: cannot upgrade code any further!");
await WriteDataAsync(response);
return;
}
// Find a new CSN based on the `NameCode` of the current character and `GradeCoreId + 1`
// For some reason, there is a seperate character for each limit/core break value.
CharacterRecord? newCharacter = fullchardata.FirstOrDefault(c => c.NameCode == currentCharacter.NameCode && c.GradeCoreId == currentCharacter.GradeCoreId + 1);
if (newCharacter != null)
{
// replace character in DB with new character
targetCharacter.Grade++;
targetCharacter.Tid = newCharacter.Id;
response.Character = new NetUserCharacterDefaultData()
{
Csn = req.Csn,
CostumeId = targetCharacter.CostumeId,
Grade = targetCharacter.Grade,
Lv = user.GetSynchroLevel(),
Skill1Lv = targetCharacter.Skill1Lvl,
Skill2Lv = targetCharacter.Skill2Lvl,
Tid = targetCharacter.Tid,
UltiSkillLv = targetCharacter.UltimateLevel
};
// remove spare body item
ItemData bodyItem = user.Items.FirstOrDefault(i => i.Isn == req.Isn) ?? throw new NullReferenceException();
user.RemoveItemBySerialNumber(req.Isn, 1);
response.Items.Add(NetUtils.ToNet(bodyItem));
if (newCharacter.GradeCoreId == 103 || newCharacter.GradeCoreId == 11 || newCharacter.GradeCoreId == 201)
{
user.AddTrigger(Trigger.CharacterGradeMax, 1);
}
JsonDb.Save();
}
}
// Send the response back to the client
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,19 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Client
{
[PacketPath("/system/checkversion")]
public class CheckClientVersion : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqCheckClientVersion req = await ReadData<ReqCheckClientVersion>();
ResCheckClientVersion response = new()
{
Availability = ResCheckClientVersion.Types.Availability.None
};
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,47 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.ContentsOpen
{
[PacketPath("/contentsopen/get/unlock")]
public class GetUnlocked : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetContentsOpenUnlockInfo req = await ReadData<ReqGetContentsOpenUnlockInfo>();
User user = GetUser();
// This request is used for showing the "Collection Item Unlocked" Popup and button unlock animation
ResGetContentsOpenUnlockInfo response = new();
if (user.ContentsOpenUnlocked.Count == 0)
{
// These Always returned as true by official server
// Fixes "Recruitment unlocked" during chapter 0
// TODO: Don't hardcode this, maybe its in GameData
user.ContentsOpenUnlocked.Add(3, new(true, true));
user.ContentsOpenUnlocked.Add(4, new(true, true));
user.ContentsOpenUnlocked.Add(6, new(true, true));
user.ContentsOpenUnlocked.Add(15, new(true, true));
user.ContentsOpenUnlocked.Add(16, new(true, true));
user.ContentsOpenUnlocked.Add(18, new(true, true));
user.ContentsOpenUnlocked.Add(19, new(true, true));
JsonDb.Save();
}
foreach (KeyValuePair<int, UnlockData> item in user.ContentsOpenUnlocked.OrderBy(x => x.Key))
{
response.ContentsOpenUnlockInfoList.Add(new NetContentsOpenUnlockInfo()
{
ContentsOpenTableId = item.Key,
IsUnlockButtonPlayed = item.Value.ButtonAnimationPlayed,
IsUnlockPopupPlayed = item.Value.PopupAnimationPlayed,
});
}
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,38 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.ContentsOpen
{
[PacketPath("/contentsopen/set/unlock/button")]
public class SetUnlockButton : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSetContentsOpenUnlockButtonPlay req = await ReadData<ReqSetContentsOpenUnlockButtonPlay>();
User user = GetUser();
ResSetContentsOpenUnlockButtonPlay response = new();
// Unlock button animation completed
foreach (int item in req.ContentsOpenTableIds)
{
if (user.ContentsOpenUnlocked.TryGetValue(item, out UnlockData? data))
{
data.ButtonAnimationPlayed = true;
}
else
{
user.ContentsOpenUnlocked.Add(item, new UnlockData()
{
ButtonAnimationPlayed = true
});
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,36 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.ContentsOpen
{
[PacketPath("/contentsopen/set/unlock/popup")]
public class SetUnlockPopup : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSetContentsOpenUnlockPopupPlay req = await ReadData<ReqSetContentsOpenUnlockPopupPlay>();
User user = GetUser();
ResSetContentsOpenUnlockPopupPlay response = new();
foreach (int item in req.ContentsOpenTableIds)
{
if (user.ContentsOpenUnlocked.TryGetValue(item, out UnlockData? data))
{
data.PopupAnimationPlayed = true;
}
else
{
user.ContentsOpenUnlocked.Add(item, new UnlockData()
{
PopupAnimationPlayed = true
});
}
}
JsonDb.Save();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,16 @@
using Google.Protobuf.WellKnownTypes;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer
{
public class EmptyHandler : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetNow req = await ReadData<ReqGetNow>();
ResGetNow response = new();
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,19 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Episode
{
[PacketPath("/episode/mission/enter")]
public class ListMission : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqListValidEpMission req = await ReadData<ReqListValidEpMission>();
ResListValidEpMission response = new();
// TOOD
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,19 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/bookmark/event/scenario/exist")]
public class CheckBookmarkScenarioExists : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqExistScenarioBookmark req = await ReadData<ReqExistScenarioBookmark>();
ResExistScenarioBookmark response = new();
// TODO
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,21 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/scenario/exist")]
public class CheckScenarioExists : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqExistEventScenario req = await ReadData<ReqExistEventScenario>();
ResExistEventScenario response = new();
foreach (string? item in req.ScenarioGroupIds)
response.ExistGroupIds.Add(item);
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,20 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.CollectSystem
{
[PacketPath("/event/collect-system/list-field")]
public class ListField : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqListFieldEventCollectData req = await ReadData<ReqListFieldEventCollectData>();
User user = GetUser();
ResListFieldEventCollectData response = new();
// TODO
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,36 @@
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/scenario/complete")]
public class CompleteEventScenario : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqSetEventScenarioComplete req = await ReadData<ReqSetEventScenarioComplete>();
User user = GetUser();
if (user.EventInfo.TryGetValue(req.EventId, out EventData? evt))
{
if (!evt.CompletedScenarios.Contains(req.ScenarioId))
{
evt.CompletedScenarios.Add(req.ScenarioId);
}
}
else
{
evt = new();
evt.CompletedScenarios.Add(req.ScenarioId);
user.EventInfo.Add(req.EventId, evt);
}
JsonDb.Save();
ResSetEventScenarioComplete response = new();
// TODO reward
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,48 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/eventfield/enter")]
public class EnterEventField : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqEnterEventField req = await ReadData<ReqEnterEventField>();
User user = GetUser();
ResEnterEventField response = new()
{
Field = new()
};
// Retrieve collected objects and completed stages
if (!user.FieldInfoNew.TryGetValue(req.MapId, out FieldInfoNew? field))
{
field = new FieldInfoNew();
user.FieldInfoNew.Add(req.MapId, field);
}
foreach (int stage in field.CompletedStages)
{
response.Field.Stages.Add(new NetFieldStageData() { StageId = stage });
}
foreach (NetFieldObject obj in field.CompletedObjects)
{
if (obj == null) continue;
if (obj.Type == 1)
response.Field.Objects.Add(obj);
}
// Retrieve camera data
if (user.MapJson.TryGetValue(req.MapId, out string? mapJson))
{
response.Json = mapJson;
}
await WriteDataAsync(response);
}
}
}

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

@@ -0,0 +1,42 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/login/get")]
public class EventLoginGet : LobbyMsgHandler
{
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()
}; // fields "EndDate", "DisableDate", "RewardHistories", "LastAttendance"
// 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

@@ -0,0 +1,20 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event.Field
{
[PacketPath("/event/field/password-door/list")]
public class ListPasswordDoor : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqListFieldPasswordDoorData req = await ReadData<ReqListFieldPasswordDoorData>();
User user = GetUser();
ResListFieldPasswordDoorData response = new();
// TODO
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,45 @@
using EpinelPS.Data;
using EpinelPS.Database;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/challengestage/get")]
public class GetChallengeStage : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqChallengeEventStageData req = await ReadData<ReqChallengeEventStageData>();
User user = GetUser();
ResChallengeEventStageData response = new()
{
RemainTicket = 3,
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
});
JsonDb.Save();
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

@@ -0,0 +1,29 @@
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/scenario/get")]
public class GetEventScenario : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
ReqGetEventScenarioData req = await ReadData<ReqGetEventScenarioData>();
User user = GetUser();
ResGetEventScenarioData response = new();
if (response.ScenarioIdList.Count == 0)
{
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());
await WriteDataAsync(response);
}
}
}

View File

@@ -0,0 +1,21 @@
using EpinelPS.Data;
using EpinelPS.Utils;
namespace EpinelPS.LobbyServer.Event
{
[PacketPath("/event/getjoinedevent")]
public class GetJoinedEvent : LobbyMsgHandler
{
protected override async Task HandleAsync()
{
await ReadData<ReqGetJoinedEvent>();
//types are defined in EventTypes.cs
ResGetJoinedEvent response = new();
User user = GetUser();
// add gacha events from active lobby banners
EventHelper.AddJoinedEvents(user, ref response);
await WriteDataAsync(response);
}
}
}

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