Compare commits

...

592 commits

Author SHA1 Message Date
Crypto137
9ed41d43a8
Merge pull request #199 from AlexBond2/pvp
PvP Commands
2026-03-31 21:33:52 +03:00
AlexBond
8595b65421 Add TeamSizeOverride in GetHashCode 2026-03-31 21:30:27 +03:00
AlexBond
75c78bec4c Call match from MatchQueueStatus 2026-03-31 21:19:20 +03:00
AlexBond
fb56fe7e50 Fix 5v5 output 2026-03-31 18:57:59 +03:00
AlexBond
34b059fc96 Implement PvPCommands 2026-03-31 18:49:29 +03:00
Crypto137
088590eca2 Adjust DisableMissionXPBonuses setting behavior 2026-03-28 17:27:14 +03:00
Crypto137
b3e9fed249 Fix DifficultyTier property not being set on spawns and summons 2026-03-27 03:05:25 +03:00
Crypto137
4ee8419132 Remove percentile based login queue reconnect permissions 2026-03-25 18:57:14 +03:00
Crypto137
5c9b4b7aff Add missing semicolon 2026-03-25 18:46:36 +03:00
Crypto137
74e39fb00a Adjust resource migration filter 2026-03-25 18:42:06 +03:00
Crypto137
346965116f Implemented !server whitelist command 2026-03-25 17:53:26 +03:00
Crypto137
a3551c8ab8 Add EnsureCapacity() call to AchievementState.Serialize() 2026-03-24 20:45:02 +03:00
Crypto137
c38018316b Add reusable picker for IntervalPowersRandomTarget hotspots 2026-03-24 20:29:50 +03:00
Crypto137
5acbeb2602 Remove reconnect permissions from the reconnect login queue 2026-03-24 18:11:01 +03:00
Crypto137
32ec15fd1a Expand login queue reconnects to clients that successfully pass the queue 2026-03-24 17:54:41 +03:00
Crypto137
e0508fb29c
Merge pull request #196 from Crypto137/dev
Optimization pass and login queue improvements
2026-03-23 18:32:19 +03:00
Crypto137
cc241e6325 Cache SessionEncryptionChanged message in LoginQueueManager 2026-03-23 18:30:37 +03:00
Crypto137
b93e433227 Implement login queue reconnects 2026-03-23 18:26:21 +03:00
Crypto137
c29d8634ad Implement BypassLoginQueue account flag 2026-03-23 17:32:52 +03:00
Crypto137
66233d2fbe Overhaul LoginQueueManager 2026-03-23 17:23:38 +03:00
Crypto137
877adaf80d Remove SequenceEqual check when serializing player entity blobs 2026-03-23 15:21:22 +03:00
Crypto137
8740e60e35 Rename FastCodedOutputStream to RecyclableCodedOutputStream and remove ArrayPool 2026-03-23 15:09:35 +03:00
Crypto137
08a875b69f Implement InvasiveList iterator pooling 2026-03-23 14:23:11 +03:00
Crypto137
c62be10413 Fix index for EntityCollection.All 2026-03-23 13:50:27 +03:00
Crypto137
a934f07f3a Overhaul InvasiveList iteration 2026-03-23 13:39:19 +03:00
Crypto137
03839a2699 Optimize InvasiveListNode 2026-03-23 00:19:22 +03:00
Crypto137
be411b2684 Optimize array allocations when saving player entity blobs 2026-03-22 20:58:21 +03:00
Crypto137
fc83aa151b Increase initial size for Archive.SharedAutoBuffer 2026-03-22 20:46:57 +03:00
Crypto137
1c75103166 Optimize allocations when writing float fields in protobuf messages 2026-03-22 20:37:27 +03:00
Crypto137
7f3837583d Disable EventScheduler trigger time logging in Release builds 2026-03-21 23:43:12 +03:00
Crypto137
85cd903586 Remove null region logging in Player.DiscoverMapPosition() 2026-03-21 23:27:34 +03:00
Crypto137
641c35258f Change loot context for the !item give command to CashShop 2026-03-20 14:40:08 +03:00
Crypto137
b19e72b447
Merge pull request #193 from Crypto137/dev
1.0.0 changes
2026-03-20 10:00:10 +03:00
Crypto137
58f9fa217c Fix autosaves triggering during region transfers 2026-03-18 18:31:51 +03:00
Crypto137
1066c0ee58 Update README 2026-03-18 16:56:55 +03:00
Crypto137
2e7cd92a08
Merge pull request #192 from AlexBond2/fixes
New UI
2026-03-18 09:47:43 +03:00
AlexBond
6750e6b62a G element 2026-03-18 02:00:52 +03:00
AlexBond
1e4ada5896 Small fix 2026-03-17 23:54:10 +03:00
AlexBond
b79f02e541 Packed SVG 2026-03-17 22:56:27 +03:00
AlexBond
08cccf392a New UI for Bundles 2026-03-17 20:56:12 +03:00
AlexBond
dac7d90b21 New UI for Add G 2026-03-17 20:20:44 +03:00
Crypto137
cb747a5ae9 Allow store purchases in the tutorial when auto unlocks are disabled 2026-03-16 03:09:10 +03:00
Crypto137
1897911201 Implement eRTV_PlayerLimit tuning variable 2026-03-15 05:06:14 +03:00
Crypto137
54a406501a Add EnableTownPlayerLimit config setting 2026-03-15 04:32:55 +03:00
Crypto137
47eaaffa59
Merge pull request #191 from AlexBond2/fixes
Fixes for Achievements
2026-03-15 01:09:34 +03:00
AlexBond
c3668122d7 Merge remote-tracking branch 'upstream/dev' into fixes 2026-03-15 01:04:29 +03:00
AlexBond
7b315562ce Fix Maggia > Hammerhead 2026-03-15 01:02:57 +03:00
AlexBond
c0ff50296e Fix Forge Region 2026-03-15 00:40:29 +03:00
AlexBond
93f227260b Fix Russian text for Achievements 2026-03-15 00:39:48 +03:00
Crypto137
2c0ab87016 Add emote unlock to the !dance command 2026-03-14 10:17:28 +03:00
Crypto137
de7d68cba1 Increase Event.InfiniteLoopCheckLimit to match the client 2026-03-14 00:09:03 +03:00
Crypto137
cf7123a5ea Add MIME types for xml and json to StaticFileWebHandler 2026-03-13 09:14:35 +03:00
Crypto137
947b284f39 Add DisableMissionXPBonuses config setting 2026-03-11 22:24:54 +03:00
Crypto137
bfa1cb3db4 Add -locale launch parameter to documentation 2026-03-10 03:12:10 +03:00
Crypto137
6f2ada7632 Add config settings for the !item creditchest command 2026-03-09 18:09:55 +03:00
Crypto137
48c2ce65b2 Remove the Linux compatibility mode account flag 2026-03-08 16:42:54 +03:00
Crypto137
d386cff233 Restrict !item creditchest command to admins 2026-03-08 16:38:00 +03:00
Crypto137
9a8beaa7e3 Fix region instances being created for empty matches 2026-03-08 16:35:37 +03:00
Crypto137
69cf3dc8b2 Add minimum schema version when loading SQLite db files 2026-03-07 18:52:25 +03:00
Crypto137
18bbcb4e42 Add archive size and game build number to persistent archive headers 2026-03-07 18:10:10 +03:00
Crypto137
00e3bcb084 Rename _bufferStream to _buffer in Archive for client consistency 2026-03-07 16:54:43 +03:00
Crypto137
bfd161d517 Remove legacy 0.x archive versioning code 2026-03-07 16:31:25 +03:00
Crypto137
5c44fcff96 Fix LiveTuningDataGlobal.json output copy settings 2026-03-07 16:26:17 +03:00
Crypto137
3496897ea8 Move global live tuning data to a separate file and remove baseline buffs 2026-03-07 16:15:17 +03:00
Crypto137
4e82fbe611 Disable character auto unlocks by default 2026-03-07 15:56:57 +03:00
Crypto137
2433769dbb Bump version to 1.0.0 2026-03-07 00:51:57 +03:00
Crypto137
49fe06b3f9 Fix currency bonus applying when it's not supposed to 2026-03-07 00:51:32 +03:00
Crypto137
563dfa3dc8 Remove player index check from PartyFilterPrototype 2026-03-06 19:11:45 +03:00
Crypto137
e4952dc538
Merge pull request #185 from Crypto137/mission-reward-scaling
Implement mission reward scaling
2026-03-04 20:13:40 +03:00
Crypto137
723c18f18e Implement stacking currency bonuses 2026-03-04 20:12:04 +03:00
Crypto137
a6247ba387 Implement mission contribution reward multiplier 2026-03-04 19:48:36 +03:00
Crypto137
f0aebfcb1f Implement mission xp multiplier 2026-03-03 18:38:41 +03:00
Crypto137
2663369222
Merge pull request #184 from AlexBond2/todo
Implement TODO for Mission Contributors, FloorToCenter
2026-03-03 18:37:16 +03:00
Crypto137
b974a72cd4 Optimize dictionary lookups in Mission.GetSortedContributors() 2026-03-03 18:26:39 +03:00
AlexBond
8babb8980c Merge remote-tracking branch 'upstream/master' into todo 2026-03-03 17:07:54 +03:00
Crypto137
19f699ebd3
Merge pull request #183 from AlexBond2/achievements
Achievements Rework
2026-03-03 14:20:40 +03:00
Crypto137
dff5bb5210 Improve load order override for AchievementStringMap.json 2026-03-03 14:07:44 +03:00
AlexBond
5f56b82052 Fix GetSortedContributors 2026-03-02 19:27:38 +03:00
AlexBond
d1e76ba627 Fix and clear 2026-03-02 18:54:16 +03:00
AlexBond
a1a461061d GetSortedContributors, GetContribution 2026-03-02 17:00:47 +03:00
AlexBond
afbe865014 GetCurrentFlyingHeight 2026-03-02 16:58:16 +03:00
AlexBond
8b08eeba39 Replace artifact to recipe 2026-02-27 22:59:22 +03:00
AlexBond
1ce460c676 Fix achievment Icon 2026-02-27 22:31:35 +03:00
AlexBond
19584cfaa9 Bounty of the Bifrost Reward 2026-02-27 22:01:46 +03:00
AlexBond
f272e0c360 Fix Blessing achievement 2026-02-27 04:21:40 +03:00
AlexBond
382680b424 Restore Holo-Sim Enthusiast 2026-02-27 03:55:07 +03:00
AlexBond
dc245333da Restore Red Solo Terminals 2026-02-26 20:07:45 +03:00
AlexBond
04832bb84d Restore Spear of Odin achievement 2026-02-26 00:26:33 +03:00
AlexBond
333d4ceec8 Restore Blessings of Asgard achievement 2026-02-25 23:51:27 +03:00
AlexBond
819f548a7c Runemaster Achievement 2026-02-25 18:36:56 +03:00
AlexBond
f0e9df87e2 Collection Achievements 2026-02-25 06:05:08 +03:00
AlexBond
24baf98f09 Merge remote-tracking branch 'upstream/master' into achievements 2026-02-25 02:37:58 +03:00
AlexBond
6c9ace34a9 Seasonal Achievements 2026-02-25 02:36:39 +03:00
AlexBond
53a8d6eec2 UI Limit Russian text 2026-02-24 22:58:07 +03:00
AlexBond
11351586ca UI Limit Russian items 2 2026-02-24 21:15:09 +03:00
AlexBond
a4a0b6ce7e UI Limit Russian items 2026-02-24 04:22:04 +03:00
AlexBond
4d57785c5b Restore Beyond Legendary achievment 2026-02-23 21:13:29 +03:00
Crypto137
c337f6413e Disable session migration for accounts in error state 2026-02-23 12:13:54 +03:00
Crypto137
c5a47a0d86 Add failsafe for when PersistenceUtility fails 2026-02-23 11:41:16 +03:00
Crypto137
4bd4da8d3b Fix null reference in PersistenceUtility.StoreInventory() 2026-02-23 11:27:16 +03:00
AlexBond
248c3ac97e Restore Phoenix achievements
The Phoenix Unleashed
The Phoenix Contained
2026-02-23 04:27:30 +03:00
AlexBond
ec4f615fa7 Restore Pet achievements
Man Vs. Pig
Puddlegulp, Aid Me!
Head Case
2026-02-23 02:54:12 +03:00
AlexBond
19456f0e04 Reward for Trained by Cap achiev 2026-02-23 02:52:43 +03:00
AlexBond
78b04a0960 Restore achievements
I'm Gonna Need Your Leg
The Impending Zombie Plague
Shock and Oomph
2026-02-23 01:35:55 +03:00
AlexBond
2b2f8fbd41 Rework Party achievements 2026-02-23 01:11:26 +03:00
AlexBond
06bfd236dd Back Surfer, Thing and move Hawkeye achievements 2026-02-22 22:24:04 +03:00
AlexBond
a7374eeb96 Rename Disable -> DisableBroken 2026-02-22 22:22:27 +03:00
Crypto137
9fa2b9ec46 Remove InventoryLocation.IsValid checks that replaced older null checks 2026-02-22 20:56:04 +03:00
AlexBond
e7712a1e34 As Blade 2026-02-21 05:37:00 +03:00
AlexBond
b0c633dc13 Merge remote-tracking branch 'upstream/master' into achievements 2026-02-21 04:00:41 +03:00
AlexBond
99eba31456 Legacy StringMap 2026-02-21 04:00:03 +03:00
Crypto137
33f9be3b6a Add null check to TuningTable.GetDamageMultiplier() 2026-02-20 01:10:41 +03:00
Crypto137
66eedf1a5c Fix MigrationData not resetting on logout in JSON mode 2026-02-20 00:45:09 +03:00
AlexBond
427db7782a Restore Achieve: Exorcism in Muspelheim 2026-02-19 23:51:04 +03:00
AlexBond
e30f285d6d Disable broken achievements 2026-02-19 21:07:40 +03:00
AlexBond
faaac2912c Merge remote-tracking branch 'upstream/master' into achievements 2026-02-19 05:46:54 +03:00
Crypto137
3a529dd456 Fix ItemCollected scoring events for stacked items in ReplaceSelfHelper() 2026-02-19 05:43:18 +03:00
AlexBond
956778dbc8 Merge remote-tracking branch 'upstream/master' into achievements 2026-02-19 05:24:44 +03:00
AlexBond
6a0fce2238 Legacy StringMap Struct, Disable InfoMap 2026-02-19 05:19:22 +03:00
AlexBond
8afb2e2d8b Legacy InfoMap Struct 2026-02-19 03:55:58 +03:00
Crypto137
3959bd4bcc Add ItemCollected scoring event to LootManager.GiveItem() 2026-02-19 01:44:25 +03:00
AlexBond
d4ceade3e5 Fantastic Four category for Party achievements 2026-02-18 23:06:14 +03:00
AlexBond
c3947e4ac5 Child Fantastic Four Achievements 2026-02-18 20:47:25 +03:00
AlexBond
9a9bd4e957 Restore original SubCategoryStr for F4 2026-02-18 19:46:10 +03:00
AlexBond
29f691576c Restore Fantastic Four Achievements 2026-02-18 19:28:40 +03:00
AlexBond
eb8bfba84d Fix: Collection Cakewalk, Cake Collection Cubed 2026-02-18 03:58:16 +03:00
Crypto137
2272c8bbb8 Change Bounds to struct 2026-02-17 00:56:14 +03:00
AlexBond
f192ba4b45 Merge Achievement ContextMap to InfoMap 2026-02-16 23:38:19 +03:00
AlexBond
b192d72b59 Add Russian Achiev strings, Fix old Engish string 2026-02-16 04:39:32 +03:00
Crypto137
6084f28cb9 Change NaviPathNode to struct 2026-02-15 23:06:50 +03:00
Crypto137
92be16045e Change LocomotionState to struct 2026-02-15 22:12:47 +03:00
Crypto137
199c37ab0e Implement MapDiscoveryData sync between party members 2026-02-14 22:01:37 +03:00
Crypto137
0d57bbe330 Add IsTransient check to PvP entity serialization 2026-02-14 20:08:10 +03:00
Crypto137
71a9297e93 Remove player data save in PlayerConnection.BeginRegionTransfer() 2026-02-11 18:51:35 +03:00
Crypto137
beeb940233 Change InventoryLocation to struct 2026-02-11 18:33:21 +03:00
Crypto137
20048a5082 Implement AvatarWorldInstanceId incrementation 2026-02-11 13:59:35 +03:00
Crypto137
29156c085c Implement player data autosaving 2026-02-10 19:18:57 +03:00
Crypto137
de20102449 Switch to using SemaphoreSlim for DBAccount locking 2026-02-10 17:16:02 +03:00
Crypto137
1f2827cb22 Remove message list debug logging from MuxPacket 2026-02-09 14:41:41 +03:00
Crypto137
b990ee4044 Change NaviPathGenerator to struct 2026-02-09 13:46:23 +03:00
Crypto137
4d9592722a Implement lazy PowerCollection allocation 2026-02-09 13:16:39 +03:00
Crypto137
aa32ca3570 Implement IsMelee() override for Avatar 2026-02-09 12:51:49 +03:00
Crypto137
5bdcc011fc Change NaviTriangleState to readonly struct 2026-02-08 17:36:29 +03:00
Crypto137
2fa202d28a Change NaviEdgePathingFlags to struct 2026-02-08 16:55:39 +03:00
Crypto137
7dcca6e8d0 Add initial capacity for PropertyList node dictionaries 2026-02-08 15:56:18 +03:00
Crypto137
a5990308a8 Optimize list allocations in NaviPathGenerator.GeneratePathInternal() 2026-02-08 14:15:56 +03:00
Crypto137
0974f05d95 Add archive data buffer overwrite when the size matches 2026-02-07 15:26:01 +03:00
Crypto137
89f142e795 Increase initial capacity for pooled message lists to 4096 2026-02-07 14:51:43 +03:00
Crypto137
45a0d9f784 Rename PersistenceHelper to PersistenceUtility 2026-02-07 13:37:24 +03:00
Crypto137
536cc14019 Optimize DBEntityCollection updates 2026-02-07 13:32:38 +03:00
Crypto137
aea202985a Remove array heap allocation in Aabb2.GetPoints() 2026-02-06 19:11:48 +03:00
Crypto137
057f880087 Inline the state array in ProceduralAI 2026-02-06 18:59:12 +03:00
Crypto137
dda728119f Inline the action key array in AbilityKeyMapping 2026-02-06 18:46:42 +03:00
Crypto137
5cbee2dc64 Replace arrays with spans in the Serializer API 2026-02-06 18:39:35 +03:00
Crypto137
be12f49374 Inline avatar endurance event pointer arrays 2026-02-06 18:29:58 +03:00
Crypto137
0afab689ea Clean up RegionLocation 2026-02-05 11:18:22 +03:00
Crypto137
ebab896841 Change RegionLocation to struct 2026-02-05 10:12:05 +03:00
Crypto137
9898a0d3f8 Remove the unused unknown int field from WorldEntity 2026-02-05 07:58:22 +03:00
Crypto137
a2700ce473 Change RegionLocationSafe to struct 2026-02-05 07:50:12 +03:00
Crypto137
27f72885ce Add initial capacity for pooled message lists 2026-02-04 16:15:43 +03:00
Crypto137
9fab6b22aa Implement MuxPacket message list pooling 2026-02-04 16:04:56 +03:00
Crypto137
59f2f3f838 Add workaround for compiler crash on Ubuntu 2026-02-03 14:16:28 +03:00
Crypto137
2b6fc6d414 Inline console account id arrays 2026-02-03 09:03:01 +03:00
Crypto137
e0b2a389b3 Replace inline arrays with spans in GetWedgeVertices() and GetWedgeTriangles() 2026-02-03 08:55:27 +03:00
Crypto137
b9ad2441e2 Optimize array allocations for wedge collision checks 2026-02-03 08:37:22 +03:00
Crypto137
2be1050893 Remove unnecessary inline array constructor calls 2026-02-03 06:38:58 +03:00
Crypto137
a89794d076 Inline EntityPhysics arrays 2026-02-03 06:34:09 +03:00
Crypto137
5df4937324 Optimize array allocations in NaviPathGenerator.CanCrossTriangle() 2026-02-03 06:17:57 +03:00
Crypto137
9304f8f5e9 Replace individual fields in PropertyStore with inline arrays 2026-02-03 06:02:17 +03:00
Crypto137
48017a1f44 Inline PropertyInfo arrays 2026-02-03 05:52:26 +03:00
Crypto137
09bfcb4091 Add generic inline arrays 2026-02-02 16:50:23 +03:00
Crypto137
58bb2668ca Disable Endurance migration 2026-02-02 13:21:13 +03:00
Crypto137
cd4dc0eb11 Make use of PoolSpan in NaviCdt.CheckDelaunaySwap() 2026-02-02 11:57:31 +03:00
Crypto137
ef9efaf5a3 Inline arrays in NaviTriangle, NaviEdge, and NaviEdgePathingFlags 2026-02-02 11:56:34 +03:00
Crypto137
9633d880c6 Simplify Archive's implementation of IDisposable 2026-02-02 11:11:01 +03:00
Crypto137
e667647ef7 Add reusable read buffer for Archive 2026-02-02 06:53:55 +03:00
Crypto137
0830e30d96 Inline child arrays in QuadtreeNode 2026-02-01 12:06:15 +03:00
Crypto137
10fcbd82b5 Refactor and optimize Quadtree.ElementIterator 2026-02-01 11:25:50 +03:00
Crypto137
4da7fa62f4 Rename EntityOctree.cs to EntityRegionSpatialPartition.cs 2026-02-01 07:25:00 +03:00
Crypto137
4d12b62812 Rename generic type parameters used in Quadtree 2026-02-01 06:47:50 +03:00
Crypto137
dac441614c Clean up Quadtree implementation 2026-02-01 05:48:41 +03:00
Crypto137
2277b27adf Refactor and optimize EntityRegionSpatialPartition.ElementIterator 2026-01-31 02:50:28 +03:00
Crypto137
85725d07ea Enable player-restricted entity spatial partitions 2026-01-30 04:55:41 +03:00
Crypto137
dccb54ff2e Clean up EntityRegionSPContextFlags 2026-01-30 04:53:24 +03:00
Crypto137
f685bc3ba9 Add avatar iterator count warning messages to EntityRegionSpatialPartition 2026-01-30 02:53:24 +03:00
Crypto137
281c106acf Optimize RegionAvatarIterator 2026-01-30 01:38:58 +03:00
Crypto137
a32ff9d2b1 Add missed GetValueRefOrAddDefault() optimization to AchievementManager 2026-01-29 04:48:51 +03:00
Crypto137
1061dd1fd0 Optimize PropertyEnumNode updates in PropertyList 2026-01-29 03:15:26 +03:00
Crypto137
8e00d34f1b Optimize dictionary value access with CollectionsMarshal.GetValueRefOrAddDefault() 2026-01-28 05:02:00 +03:00
Crypto137
89eea1f36b Optimize remaining list allocations in NaviCdt 2026-01-27 22:47:56 +03:00
Crypto137
aaff65ad6d Fix PvP match count being incremented in non-PvP regions 2026-01-27 20:20:13 +03:00
Crypto137
ce265be842 Optimize NaviPoint and NaviEdge array allocations in NaviCdt 2026-01-26 22:13:12 +03:00
Crypto137
31d47abf23 Inline accumulated policy counts in InterestReferences 2026-01-26 17:25:10 +03:00
Crypto137
f68f8ec608 Optimize list allocations in NaviCdt.RemovePoint() 2026-01-26 16:41:48 +03:00
Crypto137
6b62a625e8 Split FixedPriorityQueue and FixedDeque into separate files 2026-01-26 16:19:51 +03:00
Crypto137
0e9a07a4d2 Refactor and optimize PropertyCollection.ProtectionScope 2026-01-26 15:33:08 +03:00
Crypto137
6bdf11f382 Make use of more Span functionality in PowerResults.DamageForClient 2026-01-26 15:14:16 +03:00
Crypto137
d3250c69d5 Fix orb experience level going into negative for low level avatars 2026-01-26 14:54:16 +03:00
Crypto137
a3af696f40 Replace manual pooled collection release with handles in non-gameplay code 2026-01-25 19:41:50 +03:00
Crypto137
debb5fe204 Replace manual pooled collection release with handles in gameplay code 2026-01-25 19:26:32 +03:00
Crypto137
c74f23c90a Replace try-finally blocks with collection handles 2026-01-25 15:14:13 +03:00
Crypto137
29db37304b Add CollectionHandle overloads for ListPool getters 2026-01-25 13:36:11 +03:00
Crypto137
8cd3f5bb26 Add HitCellInfo list pooling 2026-01-24 21:14:43 +03:00
Crypto137
f4a8764c8e Implement disposable handles for pooled collections 2026-01-24 21:11:45 +03:00
Crypto137
46da027341 Optimize PhysicsContext 2026-01-24 20:36:22 +03:00
Crypto137
156dbef90b Implement on-demand HashSet allocation for PropertyCollection 2026-01-24 19:58:08 +03:00
Crypto137
ea3262fc62 Implement stack pooling 2026-01-24 19:11:50 +03:00
Crypto137
43600427f2 Optimize PowerResults field allocations 2026-01-23 17:35:21 +03:00
Crypto137
36a9a6b570 Change NaviSerialCheck to struct and add disposal validation 2026-01-23 16:37:42 +03:00
Crypto137
10e57d5fe8 Change LocomotionOptions to struct 2026-01-23 15:55:18 +03:00
Crypto137
9963ca6b5a Optimize Picker.WeightedElement 2026-01-23 15:10:52 +03:00
Crypto137
6ee71767a5 Fix PrototypeId boxing in TagInfo 2026-01-22 23:24:13 +03:00
Crypto137
061711ff90 Disable AutoFlush for FileTarget 2026-01-22 21:38:57 +03:00
Crypto137
81450dbb03 Optimize LoggingLevel enum writing in LogMessage 2026-01-22 21:24:24 +03:00
Crypto137
78a8e3ec00 Change NaviSweep to readonly struct 2026-01-22 19:23:42 +03:00
Crypto137
493ad8c52b Implement writing LogMessage directly to TextWriter 2026-01-22 18:52:38 +03:00
Crypto137
b46ff206b9 Optimize LogMessage formatting 2026-01-22 16:52:22 +03:00
Crypto137
3c9b35c2ac Optimize LogRouter memory usage 2026-01-22 16:29:15 +03:00
Crypto137
91e694922f Clean up EntityHelper.CreateOrb() calls 2026-01-22 13:54:17 +03:00
Crypto137
943ed1e08d Force the default difficulty tier for the !tower command 2026-01-21 13:57:26 +03:00
Crypto137
78ff05ccc9 Implement Ultimate Prestige 2026-01-21 13:42:28 +03:00
Crypto137
d9a51c1634 Implement gem socketing 2026-01-20 14:49:20 +03:00
Crypto137
80357e9fb9 Fix Item.OnAffixAdded() validation message formatting 2026-01-20 12:49:17 +03:00
Crypto137
2a449ac30c Fix guild replication on duplicate login in hub regions 2026-01-20 12:22:42 +03:00
Crypto137
54a36f3b97 Implement PartyKickGracePeriod 2026-01-20 12:03:40 +03:00
Crypto137
a3002a1fcd Implement mission reward properties 2026-01-19 13:54:08 +03:00
Crypto137
cd1553fd00 Implement the RealMoney loot type 2026-01-19 09:59:06 +03:00
Crypto137
9d4d8a4a6e Add server GC information to advanced setup instructions 2026-01-18 16:09:59 +03:00
Crypto137
98e08f06d5 Clean up the NetMessage handler switch 2026-01-18 15:55:17 +03:00
Crypto137
405990cbac Add patches to restore the SpecialsCostumesCardsTokens loot table 2026-01-17 11:15:02 +03:00
Crypto137
cc21bb802d Add a patch to enable ResetsWithRegion for RaftNPEQuinjetKismetController 2026-01-17 08:34:12 +03:00
Crypto137
1102a38c21 Switch to Clock.UtcNowPrecise for random seeds 2026-01-16 03:53:33 +03:00
Crypto137
38c015879f Implement chat tips 2026-01-16 02:59:19 +03:00
Crypto137
cc96adf6cb Implement player name blacklist 2026-01-16 01:44:51 +03:00
Crypto137
28087734a1 Expand property count overflow exception message 2026-01-15 03:43:33 +03:00
Crypto137
a335cf3145 Remove the fractional part from time in PropertyInfoTable initialization log 2026-01-15 03:41:39 +03:00
Crypto137
a83052e01c Implement dynamic PropertyInfo allocation for data-only properties 2026-01-15 03:26:43 +03:00
Crypto137
c8245db796 Move Omega/Infinity code to Avatar.cs 2026-01-15 02:03:04 +03:00
Crypto137
f5645fed00 Add web API documentation 2026-01-15 00:54:23 +03:00
Crypto137
cbb7b72c9b Update ServerCommands.md 2026-01-15 00:05:58 +03:00
Crypto137
b83792e7d6
Merge pull request #182 from Crypto137/dev
Implement restricted web API endpoints
2026-01-14 22:08:38 +03:00
Crypto137
5966c7f746 Add remaining account management web API endpoints 2026-01-14 00:28:26 +03:00
Crypto137
84f0add726 Make AccountManager logging more verbose 2026-01-14 00:14:13 +03:00
Crypto137
fda28199a1 Implement /AccountManagement/SetPlayerName web API endpoint 2026-01-14 00:03:01 +03:00
Crypto137
3445ad12f5 Implement /AccountManagement/SetPassword web API endpoint 2026-01-13 23:47:37 +03:00
Crypto137
8ed9a6b836 Implement web API request authorization 2026-01-13 22:49:31 +03:00
Crypto137
8b93088a90 Move WebApiKeyManager to Core 2026-01-13 21:58:20 +03:00
Crypto137
2c391a756e Implement web API key management 2026-01-13 21:44:13 +03:00
Crypto137
9f0b7f2a8d Fix FileHelper not overwriting files completely when serializing JSON 2026-01-13 21:15:46 +03:00
Crypto137
51e77730c8 Rename WebTokenManager to TokenManager and implement import/export functionality 2026-01-13 20:35:06 +03:00
Crypto137
aa55a010c9 Clean up WebTokenManager 2026-01-13 20:05:02 +03:00
Crypto137
e4e293a82b Add /index.html to the default value of NewsUrl setting 2026-01-13 05:14:48 +03:00
Crypto137
f0dd573a01 Overhaul web frontend account operation handling 2026-01-13 00:27:26 +03:00
Crypto137
6624e639b8 Add /index.html to the default value of HomePageUrl setting 2026-01-12 23:25:17 +03:00
Crypto137
b5f9b88b39 Fix index.html files from dashboard subdirectories not being loaded 2026-01-12 23:08:29 +03:00
Crypto137
9cf0ab1a33 Implement recursive dashboard file loading 2026-01-12 20:39:56 +03:00
Crypto137
0ccb95a2d7 Add common image MIME types to StaticFileWebHandler 2026-01-12 20:11:07 +03:00
Crypto137
ccf5241e82 Fix userlevel command usage in advanced setup instructions 2026-01-03 08:16:28 +03:00
Crypto137
d62e430c84 Optimize TryQueryAccountByEmail() 2025-12-24 19:17:27 +03:00
Crypto137
2ee2387e76 Add enum name parsing for the !account userlevel command 2025-12-24 18:32:32 +03:00
Crypto137
623dff503c Bump version to 0.8.1 2025-12-24 17:54:58 +03:00
Crypto137
3c84db850a Update account creation links in setup instructions 2025-12-23 20:19:15 +03:00
Crypto137
24dd563b4e Hide party and guild chat message contents from logs by default 2025-12-23 17:07:17 +03:00
Crypto137
88327b6fad Update README 2025-12-22 15:41:47 +03:00
Crypto137
c56db42132
Merge pull request #181 from Crypto137/guilds
Implement guilds
2025-12-22 15:16:26 +03:00
Crypto137
d68f04b3df Clean up guild debug logging 2025-12-22 14:53:39 +03:00
Crypto137
8d3eaf641f Change GuildMember parent table to Account instead of Player 2025-12-22 13:38:41 +03:00
Crypto137
9b23a5949a Add more readable name for ChatRoom string output 2025-12-21 21:02:27 +03:00
Crypto137
4f4208d481 Add room id output to chat logs 2025-12-21 20:58:21 +03:00
Crypto137
02858e9039 Expand chat room instances to parties 2025-12-21 20:53:49 +03:00
Crypto137
17ce7371e6 Expand chat room system to regions 2025-12-21 20:35:13 +03:00
Crypto137
a88702fa9f Implement guild chat rooms 2025-12-21 20:25:38 +03:00
Crypto137
f25c6a1ce7 Implement last logout time queries 2025-12-21 18:18:23 +03:00
Crypto137
77b003a682 Explicitly disable SQLite foreign key constraints 2025-12-21 16:45:19 +03:00
Crypto137
4da729bb1f Fix database member cleanup on guild disband 2025-12-21 16:20:38 +03:00
Crypto137
d1999d709f Clean up DBGuild member list after loading 2025-12-21 15:42:44 +03:00
Crypto137
35c1a0f5a4 Implement guild membership changes 2025-12-21 15:36:39 +03:00
Crypto137
78980c62bb Add GuildNameRegistry class 2025-12-21 13:32:29 +03:00
Crypto137
bafde065c3 Fix crash in CalculateResultDamageTransfer() 2025-12-21 10:46:26 +03:00
Crypto137
b61eddef13 Fix offline guild members not showing up in the guild tab 2025-12-20 21:31:40 +03:00
Crypto137
33a027fe64 Implement guild name change 2025-12-20 20:18:28 +03:00
Crypto137
3576641da7 Implement changing guild MOTD 2025-12-20 19:23:42 +03:00
Crypto137
8ceecb3273 Streamline guild replication to game instances 2025-12-20 18:52:19 +03:00
Crypto137
3adc0873c3 Implement adding new members to a guild 2025-12-20 17:16:32 +03:00
Crypto137
527dceb499 Implement guild invites 2025-12-20 16:02:04 +03:00
Crypto137
eaf66ca0ad Clean up MasterGuild 2025-12-20 14:36:49 +03:00
Crypto137
0dd24042fb
Merge pull request #180 from AlexBond2/fixes
Fix Zombie Sister
2025-12-19 20:55:56 +03:00
AlexBond
8c3efa5790 Fix Zombie Sister 2025-12-19 20:53:49 +03:00
Crypto137
e12bbd6ebb Implement GuildMemberNameChanged handling 2025-12-19 18:37:51 +03:00
Crypto137
d938d46df6 Implement guild name/motd change replication 2025-12-19 18:24:39 +03:00
Crypto137
b93a77f022 Implement guild membership change replication 2025-12-19 18:05:01 +03:00
Crypto137
ef04c973d0 Implement GuildFormResult handling 2025-12-19 16:49:03 +03:00
Crypto137
65c11880fa Implement guild community circle 2025-12-19 15:43:33 +03:00
Crypto137
03b2a4be3d Implement guild replication 2025-12-19 15:05:57 +03:00
Crypto137
ce5a089c52 Add GuildCompleteInfo caching 2025-12-19 13:14:49 +03:00
Crypto137
0ebfef92ba Fix command parsing with leading whitespace 2025-12-19 09:36:47 +03:00
Crypto137
22c57bbd3f
Merge pull request #179 from AlexBond2/fixes
TwinEnemyBoost
2025-12-19 09:31:22 +03:00
Crypto137
ea1dc2e7a0 Fix crash when parsing commands with trailing white spaces 2025-12-19 09:29:38 +03:00
AlexBond
5527ef6591 Implement TwinEnemyBoost 2025-12-19 02:09:25 +03:00
Crypto137
c5489bf099 Implement online guild member tracking 2025-12-18 19:16:09 +03:00
Crypto137
10317bff2e Split GuildMessageToClient from GuildMessageToServer 2025-12-18 18:01:13 +03:00
Crypto137
dd45c5a6bd Implement guild member registration in the player manager 2025-12-18 17:39:21 +03:00
Crypto137
451fd809d3 Implement guild name validation in the player manager 2025-12-18 16:24:47 +03:00
Crypto137
8019432868 Expand client guild message validation 2025-12-18 15:24:50 +03:00
Crypto137
3499b347bf Implement guild formation 2025-12-18 13:39:12 +03:00
Crypto137
f3da6900ad Implement MasterGuildManager initialization 2025-12-18 12:40:43 +03:00
Crypto137
7a7434ebf8
Merge pull request #178 from AlexBond2/fixes
Fix old CH09Main2BattleForAsgard
2025-12-17 19:01:06 +03:00
AlexBond
93fdb23fb1 Fix old CH09Main2BattleForAsgard 2025-12-17 18:59:41 +03:00
Crypto137
f339e26be1 Fix method names for SQLite guild exception logging 2025-12-17 17:50:18 +03:00
Crypto137
8505ad673f Extend database API to support guilds 2025-12-17 16:53:13 +03:00
Crypto137
3fb9bcb60a Flag GuildId as a foreign key in the GuildMember table 2025-12-17 15:21:06 +03:00
Crypto137
6d6a3113dc Add guild data schemas to the database 2025-12-17 15:10:07 +03:00
Crypto137
67741f8cab Implement guild service message routing 2025-12-17 13:59:20 +03:00
Crypto137
83f263108b Implement guild name and motd validation 2025-12-17 12:50:29 +03:00
Crypto137
4e0f5ebd77 Implement NetMessageEntityGuildInfo sending 2025-12-16 19:44:42 +03:00
Crypto137
492d662a02 Implement NetMessageGuildMessageToPlayerManager handling 2025-12-16 19:25:00 +03:00
Crypto137
f267f7063e Implement GuildMember management 2025-12-16 17:22:20 +03:00
Crypto137
74aa86d336 Begin implementing GuildManager 2025-12-16 16:12:20 +03:00
Crypto137
1c1054a5bd
Merge pull request #177 from Crypto137/trade
Implement player trade
2025-12-15 16:56:59 +03:00
Crypto137
f793f1db3b Enable Not Droppable binding state 2025-12-15 16:55:15 +03:00
Crypto137
4c0dd2be06 Implement player trade item exchange 2025-12-15 16:47:49 +03:00
Crypto137
82889f8049 Implement player trade invites 2025-12-15 14:56:23 +03:00
Crypto137
f44eee28dd Merge branch 'master' into trade 2025-12-15 13:03:15 +03:00
Crypto137
1072a97391 Remove avatars from world on forced transfers 2025-12-14 17:52:25 +03:00
Crypto137
e27971a444 Fix killing spree crash in PvP 2025-12-14 08:13:31 +03:00
Crypto137
c64c411a58 Implement trade AOI channel 2025-12-12 13:03:48 +03:00
Crypto137
581c7129cd Add handlers for the remaining trade messages 2025-12-12 12:39:16 +03:00
Crypto137
7661197339 Implement client shared trade functionality 2025-12-12 12:11:10 +03:00
Crypto137
6ee89c0cdd Make Djb2 hash implementation more consistent for non-ASCII characters 2025-12-11 07:01:17 +03:00
Crypto137
7d8123abb9 Make subcommand lookups case insensitive 2025-12-11 04:44:20 +03:00
Crypto137
ead80c5edc Optimize command extraction 2025-12-11 04:41:49 +03:00
Crypto137
bb15a9eec9 Optimize DataRefManager reverse lookups 2025-12-11 04:35:38 +03:00
Crypto137
35fc001492 Make AchievementDatabase.GetItemCollectedAchievements() thread-safe 2025-12-10 12:30:31 +03:00
Crypto137
1bc4735c21
Merge pull request #176 from Crypto137/matchmaking
Matchmaking
2025-12-10 06:31:20 +03:00
Crypto137
7fb6d94b19 Remove SetPlayerAccess debug logging 2025-12-10 06:30:28 +03:00
Crypto137
fa29e84d39
Merge pull request #175 from AlexBond2/fixes
MetaStateCombatQueueLockout, MetaStateRegionPlayerAccess
2025-12-10 03:37:42 +03:00
AlexBond
c1fe8a1c08 Implement MetaStateRegionPlayerAccess 2025-12-09 23:01:40 +03:00
AlexBond
30385844a9 Implement MetaStateCombatQueueLockout 2025-12-09 23:00:28 +03:00
AlexBond
65465e9965 EntityEnteredCombatEvent, EntityExitedCombatEvent 2025-12-09 22:59:44 +03:00
Crypto137
1021f819b5 Implement queue group size validation 2025-12-09 13:12:07 +03:00
Crypto137
7c4b8e38b8 Implement game-side region access change requests 2025-12-09 12:28:35 +03:00
Crypto137
36c873b510 Fix AOI log spam when exiting match regions 2025-12-09 11:34:56 +03:00
Crypto137
c7025ee897 Inline GetMatchesLookingForMore() 2025-12-09 11:17:38 +03:00
Crypto137
d9d653561e Separate pending matches by queue params 2025-12-09 11:14:43 +03:00
Crypto137
6a01e4b8a9 Clean up matchmaking logging 2025-12-09 10:43:27 +03:00
Crypto137
df81c856be Implement InMatch state transition for RegionRequestGroup 2025-12-08 08:56:03 +03:00
Crypto137
10595a0245 Fix ServiceEventScheduler scheduling in triggers 2025-12-08 08:55:40 +03:00
Crypto137
03dafec059 Implement RegionRequestQueue updates 2025-12-08 08:19:41 +03:00
Crypto137
d0c1666c8e Implement RegionRequestGroup bucketing 2025-12-08 07:01:01 +03:00
Crypto137
a5275ed88e Add RegionRequestQueueParams struct 2025-12-08 05:57:57 +03:00
Crypto137
6ea34a2dad Implement remaining RegionRequestGroup states 2025-12-08 04:27:58 +03:00
Crypto137
ecc77d164d Adjust TokenBucket refill calculation to prevent potential overflows 2025-12-08 01:42:20 +03:00
Crypto137
cba383f500 Implement RequestToJoinGroup queue command 2025-12-08 00:41:02 +03:00
Crypto137
ce55d33d8e Remove game-side team index assignment 2025-12-08 00:41:02 +03:00
Crypto137
95fefd8733 Implement match sorting 2025-12-08 00:41:02 +03:00
Crypto137
5ed6e7b733 Disable region transfers for players who have accepted a match invite 2025-12-08 00:41:02 +03:00
Crypto137
4f79a37341 Implement region player access change handling for matchmaking 2025-12-08 00:41:02 +03:00
Crypto137
0ed5e79c6b Add waitlist checks when adding players to a RegionRequestGroup 2025-12-08 00:41:02 +03:00
Crypto137
916e32fd10 Implement waitlist functionality for bypass queue groups 2025-12-08 00:41:01 +03:00
Crypto137
1e2e3797b6 Implement match team load balancing 2025-12-08 00:41:01 +03:00
Crypto137
272b16c245 Implement MatchTeam initialization 2025-12-08 00:41:01 +03:00
Crypto137
cf77ae5781 Implement bypass group cleanup 2025-12-08 00:41:01 +03:00
Crypto137
99cc018daf Add state transition from WaitingInWaitlistLockedState 2025-12-08 00:41:01 +03:00
Crypto137
bfac7687a7 Implement cleanup when leaving match regions 2025-12-08 00:41:00 +03:00
Crypto137
0fc913a847 Implement match invite responses 2025-12-08 00:41:00 +03:00
Crypto137
52925c8143 Implement match region creation 2025-12-08 00:41:00 +03:00
Crypto137
981d83cd2a Implement match creation and assignment 2025-12-08 00:41:00 +03:00
Crypto137
2feb028f3b Implement more RegionRequestGroup state functionality 2025-12-08 00:40:59 +03:00
Crypto137
1b7d7c0231 Implement queue group limit calculation 2025-12-08 00:40:59 +03:00
Crypto137
9c6899b615 Add RegionRequestGroupState dummies 2025-12-08 00:40:59 +03:00
Crypto137
8c17369330 Change RegionRequestGroupMemberState to an abstract class 2025-12-08 00:40:59 +03:00
Crypto137
01cd9fe16d Implement state expiration for RegionRequestGroupMember 2025-12-08 00:40:59 +03:00
Crypto137
4715434a22 Clean up RegionRequestGroupMember states 2025-12-08 00:40:58 +03:00
Crypto137
31ebca89d2 Implement RegionRequestGroupMember states 2025-12-08 00:40:58 +03:00
Crypto137
7dfcb3e427 Move queue status updates to RegionRequestGroup 2025-12-08 00:40:58 +03:00
Crypto137
c5f445a6a2 Implement RegionRequestGroup creation 2025-12-08 00:40:58 +03:00
Crypto137
904a730d18 Add RegionRequestQueueCommandHandler class 2025-12-08 00:40:58 +03:00
Crypto137
36876e7e0f Fix TokenBucket integer overflow and revert RateLimitBurst increase 2025-12-08 00:37:38 +03:00
Crypto137
b21a655c23 Increase RateLimitBurst for FrontendClient 2025-11-30 16:51:26 +03:00
Crypto137
3fd8b6bd2d Implement match queue service messages 2025-11-28 20:08:29 +03:00
Crypto137
44cc2b0f20 Add removal from queue on graceful disconnect 2025-11-28 18:41:47 +03:00
Crypto137
b53bdff38a Implement inventory cleanup events 2025-11-27 16:56:59 +03:00
Crypto137
25803aa30c Implement ReplicateForTransfer inventory migration 2025-11-27 16:09:52 +03:00
Crypto137
36feb997ff Implement avatar property migration 2025-11-27 15:01:11 +03:00
Crypto137
b5296b9af5 Implement MatchQueueStatus migration 2025-11-27 14:36:16 +03:00
Crypto137
14221651c0 Implement MatchQueueStatus updates 2025-11-27 12:51:00 +03:00
Crypto137
a0621e193d Add eRRQC_RequestToJoinGroup command when teleporting to party members 2025-11-27 11:51:49 +03:00
Crypto137
728beff686 Implement NetMessageRegionRequestQueueCommandClient handling 2025-11-26 18:45:18 +03:00
Crypto137
34a3e2ad71 Implement match queue method selection popup 2025-11-26 17:17:51 +03:00
Crypto137
f10e3170ae Implement additional shared MatchQueueStatus functionality 2025-11-26 15:55:28 +03:00
Crypto137
f5a04249fb Implement custom banner messages 2025-11-26 15:52:51 +03:00
Crypto137
794989c852 Add LocaleSerializer tests 2025-11-25 12:45:09 +03:00
Crypto137
4245d415c3 Implement achievement locale selection based on session data 2025-11-25 10:41:58 +03:00
Crypto137
efe7ca43f6 Overhaul achievement string loading 2025-11-25 10:30:24 +03:00
Crypto137
f5febb04d7 Implement LocaleSerializer 2025-11-25 08:45:14 +03:00
Crypto137
64649ab544 Remove workflow_dispatch trigger from self-contained Linux build action 2025-11-18 11:03:07 +03:00
Crypto137
fd6df182a8 Add workflow_dispatch trigger to self-contained Linux build action 2025-11-18 10:56:10 +03:00
Crypto137
5982de0fa4 Fix GitHub action for self-contained Linux builds 2025-11-18 10:48:44 +03:00
Crypto137
fb95d79198 Add GitHub action for self-contained Linux builds 2025-11-18 07:48:52 +03:00
Crypto137
3d294859df Add build script 2025-11-18 00:52:53 +03:00
Crypto137
e844eea45c Optimize json loading 2025-11-18 00:22:23 +03:00
Crypto137
65a5a50885 Fix cell hotspot creation 2025-11-17 05:00:32 +03:00
Crypto137
426394ba81
Merge pull request #174 from AlexBond2/fixes
HotspotData for CellPrototype
2025-11-17 02:41:40 +03:00
AlexBond
53afb87404 Implement HotspotData 2025-11-17 02:36:52 +03:00
AlexBond
3899384745 MetaGame Next for MetaStateMissionProgression 2025-11-17 02:29:10 +03:00
Crypto137
2d529b9663 Fix ContentType not being specified in WebRequestContext.SendJsonAsync() 2025-11-16 21:49:59 +03:00
Crypto137
c9813a5985 Fix game crash when failing to clone vendor item 2025-11-15 17:15:55 +03:00
Crypto137
dc3e4d975f Implement hidden PvP damage modifiers 2025-11-14 23:26:30 +03:00
Crypto137
43b1ca543c Disable the !tower command in PvP 2025-11-14 19:34:49 +03:00
Crypto137
77a1f32648 Add temporary PvP team selection logic 2025-11-14 18:41:11 +03:00
Crypto137
8127d734b7 Implement PvP avatar kill loot tables 2025-11-14 18:30:37 +03:00
Crypto137
54c518a417 Fix ScoreTable build warnings 2025-11-14 18:06:04 +03:00
Crypto137
0a7699915d Implement PvP team chat 2025-11-14 17:57:21 +03:00
Crypto137
0937428c85 Add live tuning data for PvP mob health 2025-11-13 23:56:14 +03:00
Crypto137
cf816704c7
Merge pull request #173 from AlexBond2/pvp
NetMessageAITargetChangeNotification
2025-11-13 20:58:41 +03:00
AlexBond
7d1bca3bd3 NetMessageAITargetChangeNotification 2025-11-13 20:46:13 +03:00
Crypto137
266041fc29 Adjust OnGotHit() behavior for AI agents 2025-11-13 19:44:27 +03:00
Crypto137
c4f2c9df0d Fix ForceSimulation region flag not being applied 2025-11-13 17:00:17 +03:00
Crypto137
5ce2a661bb Implement additional targeted condition properties 2025-11-13 16:47:29 +03:00
Crypto137
22586063ad Implement PermaBuffs 2025-11-12 19:13:56 +03:00
Crypto137
00d112b74d Implement !boost invulnerable and !boost mana commands 2025-11-12 17:51:23 +03:00
Crypto137
e7ac82b018 Implement extra avatar properties from the player entity 2025-11-12 17:34:17 +03:00
Crypto137
451fd1ca49 Adjust default DashboardUrlPath value 2025-11-09 17:18:08 +03:00
Crypto137
6bb8170bde
Merge pull request #172 from Crypto137/mtx-store
Overhaul MTX store implementation
2025-11-09 16:57:47 +03:00
Crypto137
3d0e37ab2c MTXStore: Restore removed hero bundles 2025-11-09 16:07:42 +03:00
Crypto137
b13dd1efef MTXStore: Implement custom AA requirement for gifting 2025-11-09 13:54:48 +03:00
Crypto137
a6220706a8 MTXStore: Implement gifting 2025-11-09 13:38:11 +03:00
Crypto137
827c33bd8b Adjust StaticFileWebHandler logging 2025-11-09 11:11:56 +03:00
Crypto137
8e6227bded Move live tuning data files to a subfolder 2025-11-09 10:52:31 +03:00
Crypto137
037dae6850 MTXStore: Fix team-up token replacements 2025-11-08 18:28:55 +03:00
Crypto137
883cad72f2 MTXStore: Implement bundle purchases 2025-11-08 18:24:06 +03:00
Crypto137
d2ae6c6973 MTXStore: Restore hero bundle exp potion quantities 2025-11-08 15:44:20 +03:00
Crypto137
ca6d3c74a7
Merge pull request #171 from AlexBond2/fixes
Reward balancing for SelectAct
2025-11-07 14:59:15 +03:00
AlexBond
6a353b9035 Reward balancing for SelectAct 2025-11-07 14:55:37 +03:00
Crypto137
9887dfe567 Implement bundle thumbnail generation 2025-11-07 13:23:39 +03:00
Crypto137
e204aad1da Clean up bundle info page building 2025-11-07 12:00:41 +03:00
Crypto137
0df640bec5 MTXStore: Restore more bundle quantity values 2025-11-07 10:48:31 +03:00
Crypto137
4148935d79 Add bundle asset builder script 2025-11-06 19:15:41 +03:00
Crypto137
6f54c611a9 Add bundle info to docs 2025-11-06 17:56:59 +03:00
Crypto137
1c3f37bf5a MTXStore: Implement bundle URL rewriting 2025-11-06 15:08:13 +03:00
Crypto137
ccf69931da MTXStore: Restore bundle quantity values from names 2025-11-06 14:27:16 +03:00
Crypto137
e837e49be4 MTXStore: Implement catalog hot reloading 2025-11-06 13:57:15 +03:00
Crypto137
f91c98da75 MTXStore: Fix Catalog.json formatting 2025-11-06 13:49:21 +03:00
Crypto137
578f8d0bf5 MTXStore: Implement catalog entry loading from all json files 2025-11-06 13:34:07 +03:00
Crypto137
8443fdee32 MTXStore: Separate catalog metadata from entry data 2025-11-06 13:23:59 +03:00
Crypto137
42d799ce46 Move rate limiter classes to their own namespace 2025-11-06 10:47:08 +03:00
Crypto137
638fbf8bb8 Implement sending custom system chat messages from gameplay code 2025-11-05 14:01:12 +03:00
Crypto137
efad5f0f2a Integrate ChatHelper functionality into GroupingChatManager 2025-11-05 13:43:13 +03:00
Crypto137
8966b53e02 Merge branch 'master' into mtx-store 2025-11-05 12:47:09 +03:00
Crypto137
bae1806727 Switch to using PlayerDbId for chat service messages 2025-11-05 12:38:39 +03:00
Crypto137
0641b7c373 Refactor authentication message routing 2025-11-05 10:52:57 +03:00
Crypto137
44ea473daf Move catalog data files to Data/Game/MTXStore/ 2025-11-04 15:34:25 +03:00
Crypto137
48a7a0317d Implement NetMessageBuyGiftForOtherPlayer handling 2025-11-04 13:51:59 +03:00
Crypto137
be6fa2e020 Implement NetMessageRequestPlayerOwnsItemStatus handling 2025-11-04 12:40:05 +03:00
Crypto137
98cc1a5d3f MTXStore: Clean up ES conversion logging 2025-11-04 12:08:13 +03:00
Crypto137
afb3c05969 Refactor manual task management using handles 2025-11-04 11:01:13 +03:00
Crypto137
be5ffb41b8 MTXStore: Implement ES conversion step configuration 2025-11-03 17:21:23 +03:00
Crypto137
1087057a23 MTXStore: Clean up ES frontend and implement ES step 2025-11-03 16:05:58 +03:00
Crypto137
b226f5c662 MTXStore: Implement ES conversion request handling game-side 2025-11-03 14:28:09 +03:00
Crypto137
bf230d6183 Remove MHServerEmu.Billing project 2025-11-03 12:21:08 +03:00
Crypto137
b5badf4092 MTXStore: Implement ES conversion service messages 2025-11-03 12:15:54 +03:00
Crypto137
093eed3477 MTXStore: Implement ES conversion requests 2025-11-03 10:45:00 +03:00
Crypto137
ce5153c419 MTXStore: Add ES conversion popup 2025-11-02 19:43:32 +03:00
Crypto137
73cd371f00 MTXStore: Update ES conversion frontend 2025-11-01 17:54:18 +03:00
Crypto137
ae4ef919a1 MTXStore: Add command for reloading Add G page 2025-11-01 15:56:59 +03:00
Crypto137
fbeccc542a MTXStore: Begin implementing ES conversion frontend 2025-10-31 17:06:33 +03:00
Crypto137
87781a18e4
Merge pull request #170 from AlexBond2/pvp
PvP
2025-10-31 16:11:49 +03:00
AlexBond
dbd418a5a8 eGTV_PVPEnabled 2025-10-31 15:54:40 +03:00
AlexBond
4a583239ba Merge remote-tracking branch 'upstream/master' into pvp 2025-10-31 15:48:17 +03:00
Crypto137
f84979d86f Implement unified async API for web <-> game service interaction 2025-10-31 10:10:08 +03:00
Crypto137
50550ab87e MTXStore: Implement ES balance queries 2025-10-30 14:12:30 +03:00
Crypto137
943fb6c326 Implement MTX store auth timeout 2025-10-30 13:30:10 +03:00
Crypto137
78485b5c8e Implement MTX store auth credentials verification 2025-10-30 13:22:23 +03:00
Crypto137
37fa9a8d67 Implement MTXStore auth request routing 2025-10-30 12:56:53 +03:00
Crypto137
2e425b05c8 Implement MTX store token generation 2025-10-27 00:42:07 +03:00
Crypto137
e70faeab00 Rename BillingConfig to MTXStoreConfig 2025-10-26 23:36:34 +03:00
Crypto137
a9851b8e31
Merge pull request #169 from Crypto137/web-api-overhaul
Overhaul web API
2025-10-26 20:46:44 +03:00
AlexBond
39df5eeb50 MaxAttackers, AttackerAllianceCount 2025-10-26 06:11:28 +03:00
AlexBond
0986492178 RegionKeywords for Vendor 2025-10-26 03:42:59 +03:00
AlexBond
6e7b5059da AttackerName for SendNPDefeatPlayer 2025-10-26 02:53:00 +03:00
Crypto137
f87cd749d3 Rename DashboardUrlDirectory to DashboardUrlPath 2025-10-26 02:28:07 +03:00
Crypto137
8b9e407f87 Adjust FrontendClient rate limiter to be traffic-based instead of packet-based 2025-10-25 23:27:42 +03:00
Crypto137
dbdc92a1bb Implement login rate limiting 2025-10-25 23:02:39 +03:00
Crypto137
6f1c743b3f Switch to hashing IP addresses when HideSensitiveInformation is on 2025-10-25 22:32:28 +03:00
Crypto137
5eec5dfb41 Fix trailing slash redirect for forwarded requests 2025-10-25 21:45:27 +03:00
AlexBond
061b45064c GetOtherTeamByRef 2025-10-25 05:23:22 +03:00
AlexBond
70bfe4efca AttackerWaveCycleMS 2025-10-25 02:34:52 +03:00
AlexBond
c32a6be457 UpdateRunestonesScore, SetPlayerScoreValue 2025-10-25 02:07:47 +03:00
Crypto137
b0e555e867 Web Dashboard: Add enter key handling to the account creation form 2025-10-24 22:43:57 +03:00
Crypto137
f1ddad6056 Move AuthTicket building to SessionManager 2025-10-24 22:14:33 +03:00
Crypto137
495469045d Clean up potential log spam from AddGWebHandler 2025-10-24 19:32:26 +03:00
Crypto137
1c7a0471e1 Optimize WebRequestContext string reading 2025-10-24 19:27:25 +03:00
AlexBond
5c0f7534b8 FindStartLocationFromPvPTeam, DiscoverEntitiesForPlayer 2025-10-24 04:08:38 +03:00
Crypto137
ba0171d5c2 Web Dashboard: Mirror server-side email validation 2025-10-23 22:12:18 +03:00
Crypto137
08f4488040 Improve email validation when creating accounts 2025-10-23 21:37:15 +03:00
Crypto137
238f27d967 Optimize performance report retrieval 2025-10-23 20:11:21 +03:00
Crypto137
437e7828ac Optimize WebRequestContext 2025-10-23 19:50:53 +03:00
Crypto137
43003ac169 Web Dashboard: Improve trailing slash handling 2025-10-23 19:08:40 +03:00
AlexBond
0d426d05e3 SendPlayerMetaGameComplete 2025-10-23 18:50:11 +03:00
AlexBond
b352178c5a SendPlayerDefeatedPlayer, SendNPDefeatPlayer 2025-10-23 16:19:16 +03:00
AlexBond
538fbce505 SendChatFromMetaGame 2025-10-23 16:18:37 +03:00
AlexBond
1259c77c11 Implement PvPDefenderGameMode 2025-10-23 05:13:29 +03:00
AlexBond
23d4954c11 GetTeammateByPlayer, GetTeamByRef 2025-10-23 05:12:47 +03:00
AlexBond
0f1b287d87 SpawnObjectUsePosition, GetPositionsByMarker 2025-10-23 02:29:37 +03:00
Crypto137
dcb03c0486 Add context echo for add G requests 2025-10-22 21:08:50 +03:00
AlexBond
5959a95d03 ListPool for GetInterestedClients, intArgs 2025-10-22 20:52:22 +03:00
Crypto137
af005b7979 Web Dashboard: Improve error handling 2025-10-22 19:38:08 +03:00
Crypto137
4043e1aaee Add basic handling for MTXStore web requests 2025-10-22 18:33:10 +03:00
Crypto137
3ba72706f6 Move auth related code to PlayerManagement.Auth 2025-10-22 15:58:34 +03:00
Crypto137
317d6852b0 Rename MHServerEmu.Auth to MHServerEmu.WebFrontend 2025-10-22 15:37:14 +03:00
AlexBond
d2847bffd2 OnResurrect, SetAllianceOverride, PvPTeam 2025-10-22 00:35:07 +03:00
Crypto137
ba3b56247d Web Dashboard: Switch to using operation codes for account creation results 2025-10-21 21:47:45 +03:00
Crypto137
8d8c647032 Web Dashboard: Restore server status formatting 2025-10-21 21:00:58 +03:00
Crypto137
0aa6cc2a27 Web Dashboard: Move refresh buttons to the right corner 2025-10-21 18:29:05 +03:00
Crypto137
8123c370b1 Web Dashboard: Clean up CSS a bit 2025-10-21 17:30:54 +03:00
AlexBond
b6979877d7 SpawnObjectUsePopulationMarker 2025-10-21 04:29:45 +03:00
AlexBond
0186048c0b HashSetPool for CheckForSingleEntityAlliance 2025-10-21 04:29:09 +03:00
AlexBond
753eae06f4 MissionXEncounterHostilityOk 2025-10-21 04:28:32 +03:00
Crypto137
022dfc19d4 Web Dashboard: Change region report output to table 2025-10-20 21:45:46 +03:00
Crypto137
f501052031 Web Dashboard: Add a separate config file 2025-10-20 18:36:44 +03:00
Crypto137
3ab39fb348 Web Dashboard: Add configurable origin suffix 2025-10-20 18:19:51 +03:00
Crypto137
7facb894b1 Implement structured server status output 2025-10-19 17:58:05 +03:00
Crypto137
7577008b6d Implement game metric selector for the web dashboard 2025-10-19 15:21:07 +03:00
AlexBond
90a678c550 InitializeEventHandler, PvPScoreEventHandler 2025-10-19 00:48:16 +03:00
AlexBond
aee7268249 Hardcoded TeamIndex 2025-10-19 00:30:03 +03:00
AlexBond
622a76ab37 eGTV_PVPEnabled 2025-10-18 23:05:58 +03:00
Crypto137
450180268a Remove the old web frontend 2025-10-17 18:02:08 +03:00
Crypto137
8b14517f3f Port server status to the web dashboard 2025-10-17 17:25:19 +03:00
Crypto137
947d196891 Move conversion to hex string to JS for the web frontend 2025-10-17 17:09:42 +03:00
Crypto137
f1f1791aec Port metrics formatting to the web dashboard 2025-10-17 16:42:41 +03:00
AlexBond
b0bf4ec734 Merge remote-tracking branch 'upstream/master' into pvp 2025-10-17 05:20:01 +03:00
AlexBond
bbaead036b Implement AddNewPlayer 2025-10-17 05:19:27 +03:00
Crypto137
4734c819b7 Port performance metrics to the web dashboard 2025-10-16 20:01:24 +03:00
Crypto137
23ff3f99b0 Switch to using arrow functions for dashboard callbacks 2025-10-16 18:13:48 +03:00
Crypto137
ddd0c85697 Refactor dashboard tab management 2025-10-16 17:17:05 +03:00
Crypto137
79e48eaca0 Add information on embedded browser versions to docs 2025-10-16 10:33:42 +03:00
Crypto137
b9fdd3f796 Port region report to the web dashboard 2025-10-15 17:50:21 +03:00
Crypto137
e0825de506 Fix dashboard css selectors 2025-10-15 16:20:41 +03:00
Crypto137
b06208fd2e Move dashboard onclick callbacks to the js file 2025-10-15 13:30:28 +03:00
Crypto137
6afd4656af Split the web dashboard into separate html/css/js files 2025-10-14 18:36:00 +03:00
Crypto137
d6e812d436 Implement multi-file dashboard serving 2025-10-14 17:08:34 +03:00
Crypto137
f4194fdb0d Separate dashboard tabs from the menu content div 2025-10-13 16:38:33 +03:00
Crypto137
769e7a94ab Port account creation form to the web dashboard 2025-10-13 15:04:35 +03:00
Crypto137
089531c737 Implement a web dashboard endpoint 2025-10-13 10:28:42 +03:00
Crypto137
50f145aee5 Clean up web frontend CSS 2025-10-12 11:41:09 +03:00
Crypto137
715cf9dea6 Mirror protobuf handler to the /AuthServer/Login/IndexPB endpoint 2025-10-12 09:46:26 +03:00
Crypto137
9182520f28 Merge AccountCreateForm.html with Response.html 2025-10-12 09:36:43 +03:00
Crypto137
1018244a22 Port old web API endpoints to the new backend 2025-10-12 09:18:08 +03:00
Crypto137
3e04935e47 Overhaul HTTP server / web service implementation 2025-10-11 09:27:57 +03:00
Crypto137
44557e310d Merge branch 'master' into web-api-overhaul 2025-10-10 23:13:14 +03:00
Crypto137
e19ceea36c Continue adjusting PowerCollection warnings 2025-10-10 22:54:16 +03:00
Crypto137
41856664c4 Add difficulty preference override for mission teleports 2025-10-10 21:18:43 +03:00
Crypto137
1694186b3d Implement transition destination selection dialog 2025-10-09 02:45:06 +03:00
Crypto137
75dafde7b2 Implement transition interaction validation 2025-10-08 23:20:45 +03:00
Crypto137
61cb82a00d Revert to previous search logic for connection target destinations 2025-10-08 21:30:24 +03:00
Crypto137
e58d31e69f Adjust transition destination search to use the last found target 2025-10-07 19:48:58 +03:00
Crypto137
b51087ba81 Merge branch 'master' into web-api-overhaul 2025-10-06 16:57:27 +03:00
Crypto137
8a09abab3a Fix caster override not being taken into account when unassigning proc powers 2025-10-06 16:22:19 +03:00
Crypto137
7f60b558cb
Merge pull request #167 from AlexBond2/fixes
Fix for AchievementPunisherMidtownTimed
2025-10-06 02:09:21 +03:00
AlexBond
1e348fdbfe Fix for AchievementPunisherMidtownTimed 2025-10-06 02:04:48 +03:00
Crypto137
74afa2179f Adjust power collection cleanup logging 2025-10-05 17:41:15 +03:00
Crypto137
14b253c53d Add power logging for infinite loops when cleaning up power collections 2025-10-05 17:03:14 +03:00
Crypto137
082168d7ae Add difficulty preference override for power teleports 2025-10-05 16:59:32 +03:00
Crypto137
6d93bf3949
Merge pull request #166 from AlexBond2/fixes
Fix for AXIS raid
2025-10-05 16:27:45 +03:00
AlexBond
45a349d309 Clear Debug 2025-10-05 15:10:09 +03:00
AlexBond
da7bc410a4 Debug ProceduralProfileNullifierPrototype 2025-10-05 04:56:19 +03:00
AlexBond
ac09369479 Fix ProceduralProfileSkrullNickFuryPrototype 2025-10-05 03:20:57 +03:00
AlexBond
0a66965c10 Fix ProceduralProfileNullifierPrototype 2025-10-05 03:01:26 +03:00
AlexBond
d9c37f7344 UpdatePlayerCollection 2025-10-05 02:11:01 +03:00
Crypto137
bef0e3051e Add more logging for issues with cleaning up power collections 2025-10-04 22:36:32 +03:00
Crypto137
da46aa6148 Implement rate limiting for account downloads 2025-10-04 20:38:10 +03:00
Crypto137
073ce141e9 Restore the more verbose error messages for account commands 2025-10-04 19:56:31 +03:00
Crypto137
26b3efa57a Add check for infinite loops when unassigning powers 2025-10-04 18:53:47 +03:00
Crypto137
23fc3ed8f9 Make AccountManager stateless 2025-10-03 22:03:08 +03:00
Crypto137
0a7dd2c421 Add account operation result codes 2025-10-03 20:31:55 +03:00
Crypto137
43f9f69e2b Clean up PowerCollection serialization code 2025-10-03 18:28:54 +03:00
Crypto137
18ff6e3378 Fix crash in DoPowerEventActionSpawnLootTable() 2025-10-03 12:58:48 +03:00
AlexBond
d5a2e5bf49 GetTeamForPlayer, DestTeamIndex 2025-10-03 03:16:29 +03:00
Crypto137
e04b0b95bf Implement SpawnLootForMissionContributors 2025-10-02 17:24:31 +03:00
Crypto137
dcba06570f Implement additional loot drop event types 2025-10-02 17:02:00 +03:00
Crypto137
84b4843314 Add RequireCombatActiveForKillCredit check when awarding loot 2025-10-02 15:54:30 +03:00
Crypto137
04bcdd6ec2 Implement orb effect reduction based on shrinkage progress 2025-10-02 15:28:00 +03:00
Crypto137
850ed0225b Add null check for created entities to SpawnSpec.Spawn() 2025-10-02 11:42:22 +03:00
Crypto137
15598c5896 Implement Avatar.IsCombatActive() 2025-10-01 18:11:57 +03:00
Crypto137
9796f3d823 Fix equipment procs being assigned twice when entering world 2025-10-01 16:22:18 +03:00
Crypto137
eccd9429c2 Fix PowerCollection cleanup issues 2025-10-01 15:35:39 +03:00
Crypto137
bf482abbf5 Replace more ToArray() calls with pooled collections 2025-10-01 14:45:42 +03:00
Crypto137
fdb9fbd39a Add difficulty preference override for death release teleports 2025-09-27 07:17:23 +03:00
Crypto137
e677af538d Port RepVar implementation from MHServerEmu2013 2025-09-26 13:00:44 +03:00
Crypto137
d2d5188d5c
Merge pull request #165 from AlexBond2/fixes
Fix Show'em Who's Boss achievement
2025-09-25 02:26:38 +03:00
AlexBond
56503a94e6 Fix Show'em Who's Boss achievement 2025-09-25 01:53:13 +03:00
Crypto137
8d7c49c365 Bump version to 0.8.0 2025-09-24 04:51:07 +03:00
487 changed files with 165598 additions and 80296 deletions

View file

@ -0,0 +1,29 @@
name: Nightly Release (Linux x64) (Self-Contained)
on:
schedule:
- cron: '15 7 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Publish Self-Contained
run: dotnet publish MHServerEmu.sln --configuration Release --runtime linux-x64 --self-contained
- name: Get current date
run: echo "DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: MHServerEmu-nightly-${{ env.DATE }}-Release-linux-x64-self-contained
path: |
./src/MHServerEmu/bin/x64/Release/net8.0/linux-x64/publish
!./src/MHServerEmu/bin/x64/Release/net8.0/linux-x64/publish/*.pdb
!./src/MHServerEmu/bin/x64/Release/net8.0/linux-x64/publish/*.xml

3
.gitignore vendored
View file

@ -402,3 +402,6 @@ FodyWeavers.xsd
.*#
.#*
# Build directory
/build

29
Build.bat Normal file
View file

@ -0,0 +1,29 @@
@echo off
set BUILD_DIR=.\src\MHServerEmu\bin\x64\Release\net8.0
set OUTPUT_DIR=.\build
echo ==================
echo Building...
echo ==================
dotnet build MHServerEmu.sln -c Release
if %errorlevel% neq 0 (
echo Build failed!
pause
exit /b %errorlevel%
)
echo ==================
echo Copying...
echo ==================
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
robocopy "%BUILD_DIR%" "%OUTPUT_DIR%" *.* /s /xf *.pdb *.xml /np /njs /njh
echo ==================
echo Build Complete
echo ==================
pause

View file

@ -1,9 +0,0 @@
# MHServerEmu Contribution Guidelines
This is a reverse engineering project, and the game we are dealing with heavily relies on certain aspects being 100% the same between the client and the server. This is both a blessing and a curse: on one hand, we have access to a lot of data and logic that traditionally would be server-only; on the other hand, we cannot significantly deviate from existing implementations without unexpected side effects. We may be able to introduce more significant deviations as the project matures, but this is where things stand right now.
For this reason, most gameplay-related changes have to be made with the client in mind, and contributors are expected to be using IDA, Ghidra, or other similar disassembler software. For static analysis you should be disassembling the Mac executable, even if you are running on Windows, because the former contains debug symbols for all class and function names that are not present in the other version.
If this is something you would be able to help with, please contact us on [Discord](https://discord.gg/hjR8Bj52t3), so that we can discuss your potential contributions. If you believe you can contribute in some other way that does not directly involve reverse engineering, we can discuss this as well. We would also be glad to answer any questions about the current state of the codebase.
**Pull requests without prior discussion are most likely going to be disregarded. You have been warned.**

View file

@ -1,6 +1,6 @@
# MHServerEmu Thanks/Credits file
## Contributors
## Developers
- AlexBond
@ -8,18 +8,34 @@
- Kawaikikinou
- SirLimbo
- yn01
## Discord MVPs
Our Discord server MVPs are not directly involved with the development of MHServerEmu, but they all helped the Marvel Heroes community in various ways.
- Astrid
- Doods
- DrUnkenMonk
- FF_Lowthor
- MonEll
- Pyrox
- Rylok
- SinisterSpatula
## Special Thanks
- Denys Smirnov for his [protod](https://github.com/dennwc/protod) tool that has been invaluable for reverse engineering the network protocol.
- mooege and diiis for inspiration.
- All the Discord MVPs and taskmasters who help us.
- Black Cat for her abilities of math and cheating death.
- All the people who worked on the game in any capacity.
- All the Gazillion developers who worked on the game in any capacity.

View file

@ -17,9 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Games", "src\MH
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.PlayerManagement", "src\MHServerEmu.PlayerManagement\MHServerEmu.PlayerManagement.csproj", "{33518D5D-07A0-4D08-9013-6CF873CC925B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Auth", "src\MHServerEmu.Auth\MHServerEmu.Auth.csproj", "{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Billing", "src\MHServerEmu.Billing\MHServerEmu.Billing.csproj", "{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.WebFrontend", "src\MHServerEmu.WebFrontend\MHServerEmu.WebFrontend.csproj", "{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Leaderboards", "src\MHServerEmu.Leaderboards\MHServerEmu.Leaderboards.csproj", "{2C74480B-B373-4DFD-9C9C-E652E889E9C1}"
EndProject
@ -67,10 +65,6 @@ Global
{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}.Debug|x64.Build.0 = Debug|x64
{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}.Release|x64.ActiveCfg = Release|x64
{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}.Release|x64.Build.0 = Release|x64
{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}.Debug|x64.ActiveCfg = Debug|x64
{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}.Debug|x64.Build.0 = Debug|x64
{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}.Release|x64.ActiveCfg = Release|x64
{BDAA0D9D-1DDE-4F9A-84ED-FA551A4D4419}.Release|x64.Build.0 = Release|x64
{2C74480B-B373-4DFD-9C9C-E652E889E9C1}.Debug|x64.ActiveCfg = Debug|x64
{2C74480B-B373-4DFD-9C9C-E652E889E9C1}.Debug|x64.Build.0 = Debug|x64
{2C74480B-B373-4DFD-9C9C-E652E889E9C1}.Release|x64.ActiveCfg = Release|x64

View file

@ -6,8 +6,6 @@ The only currently supported version of the game client is **1.52.0.1700** (also
We post development progress reports on our [blog](https://crypto137.github.io/MHServerEmu/). You can find additional information on various topics in the [documentation](./docs/Index.md). If you would like to discuss this project and/or help with its development, feel free to join our [Discord](https://discord.gg/hjR8Bj52t3).
**Please make sure to read our [contribution guidelines](./CONTRIBUTING.md) if you would like to participate in the development of this project.**
## Download
We provide two kinds of builds: stable and nightly.
@ -32,25 +30,13 @@ You can always upgrade from stable to nightly simply by downloading the latest n
[![Nightly Release (Windows x64)](https://github.com/Crypto137/MHServerEmu/actions/workflows/nightly-release-windows-x64.yml/badge.svg)](https://nightly.link/Crypto137/MHServerEmu/workflows/nightly-release-windows-x64/master?preview) [![Nightly Release (Linux x64)](https://github.com/Crypto137/MHServerEmu/actions/workflows/nightly-release-linux-x64.yml/badge.svg)](https://nightly.link/Crypto137/MHServerEmu/workflows/nightly-release-linux-x64/master?preview)
## Features
MHServerEmu is feature-complete as a single player experience, and we are actively working on getting the remaining multiplayer features up and running:
- Store Gifting
- Supergroups
- Matchmaking
- PvP
- Trade Window
You can find up to date information on what we are working on in [our roadmap](https://github.com/users/Crypto137/projects/5).
## FAQ
**Where can I download the game?**
**Is the game fully playable?**
All systems and content that were in the game when it was shut down in 2017 have been restored.
**Where can I download the game client?**
We do not provide download links for the game client for legal reasons. If you have played the game through Steam when it was live, you should be able to download it in your Steam library.
@ -58,13 +44,11 @@ We do not provide download links for the game client for legal reasons. If you h
Download the latest stable or nightly build and overwrite your existing files. Nightly builds can be potentially unstable, so it is recommended to back up your account database file located in `MHServerEmu\Data\Account.db` before updating.
**Will there be any wipes?**
We plan to force a fresh start when version 1.0 comes out in early 2026. Your data will not be deleted, but it will no longer be compatible with the server. You will be able to continue using your existing data on whatever the last 0.x version is going to be.
**Are you going to support other versions of the game, like the ones from before the Biggest Update Ever (BUE) came out?**
Yes, we do plan to implement support for other versions of the game after 1.52 is fully restored. The final pre-BUE version (1.48) has the highest priority.
Yes, we do plan to implement support for other versions, including the final pre-BUE version (1.48) from late 2016. Currently there are no timeframes for when this is going to happen. The current work-in-progress 1.48 code is available on the [v48](https://github.com/Crypto137/MHServerEmu/tree/v48) branch.
Some early work has also been done to support version 1.10 from mid 2013. You can find the code for it in the [MHServerEmu2013](https://github.com/Crypto137/MHServerEmu2013) repository.
**Are you going to add new content to the game (heroes, team-ups, powers, etc.)?**

View file

@ -22,7 +22,7 @@ Some of these parameters can be set automatically by using [Bifrost](https://git
These require further checking for compatibility with various versions of the client.
### ClientApp::Initialize
### ClientApp::Initialize()
| Parameter | Description | Values |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
@ -42,7 +42,7 @@ These require further checking for compatibility with various versions of the cl
| -novalidate | | |
| -nohardwareinfo | | |
### ClientAppSettingsPostCoreInit::Initialize
### ClientAppSettingsPostCoreInit::Initialize()
| Parameter | Description | Values |
| ---------------------- | -------------------------------------------------------------------------------------------- | ------------ |
@ -86,7 +86,7 @@ These require further checking for compatibility with various versions of the cl
| -streaming | | |
| -platform= | | orbis, dingo |
### SiteConfig::ParseCommandLine
### SiteConfig::ParseCommandLine()
| Parameter | Description | Values |
| ------------ | ----------- | ------ |
@ -94,44 +94,50 @@ These require further checking for compatibility with various versions of the cl
| -authserver= | | |
| -authurl= | | |
### ClientGame::Initialize
### ClientGame::Initialize()
| Parameter | Description | Values |
| ----------- | ----------- | ------ |
| -nooverwolf | | |
### Atlas::Initialize
### Atlas::Initialize()
| Parameter | Description | Values |
| ------------- | ----------- | ------ |
| -nomapmarkers | | |
### AudioManager::Initialize
### AudioManager::Initialize()
| Parameter | Description | Values |
| -------------- | ----------- | ------ |
| -nomusic | | |
| -foleyineditor | | |
### UnrealAudioManager::Initialize
### UnrealAudioManager::Initialize()
| Parameter | Description | Values |
| ------------------ | ----------- | ------ |
| -audioglobalfocus | | |
| -audiofilepackages | | |
### UnrealGameAdapter::Initialize
### UnrealGameAdapter::Initialize()
| Parameter | Description | Values |
| ---------------- | ----------- | ------ |
| -dependencygraph | | |
### UnrealGameAdapter::SendSteamUserInfo
### UnrealGameAdapter::SendSteamUserInfo()
| Parameter | Description | Values |
| ----------------------- | ----------- | ------ |
| -steamachievementupdate | | |
### InitGazillionSystems()
| Parameter | Description | Values |
| --------- | ------------------------ | ------------------------------------------------------------------------------ |
| -locale= | Overrides client locale. | Name of locale file under `Data\Game\Loco` without extension (e.g. `eng.all`). |
## Logging Channels
You can specify channels for verbose logging using the following syntax: `-LoggingChannels=-ALL,+GAME`. Available channels:

View file

@ -17,6 +17,7 @@ If this is your first time using MHServerEmu, you may be interested in this.
MHServerEmu-specific topics.
- [Server Commands](./ServerEmu/ServerCommands.md) - a list of available server commands.
- [Web API](./ServerEmu/WebApi.md) - web API documentation.
## Game

View file

@ -1,6 +1,6 @@
# Server Commands
This list was automatically generated on `2025.08.16 18:28:24 UTC` using server version `0.7.0`.
This list was automatically generated on `2026.03.25 14:52:06 UTC` using server version `1.0.0`.
To see an up to date list of all commands, type !commands in the server console or the in-game chat. When invoking a command from in-game your account has to meet the user level requirement for the command.
@ -12,7 +12,7 @@ Account management commands.
| !account ban [email] | Bans the specified account. | Moderator | Any |
| !account create [email] [playerName] [password] | Creates a new account. | Any | Any |
| !account download | Downloads a JSON copy of the current account. | Any | Client |
| !account info | Shows information for the logged in account. | Any | Client |
| !account info | Shows information for the logged in account. | Moderator | Client |
| !account password [email] [password] | Changes password for the specified account. | Any | Any |
| !account playername [email] [playername] | Changes player name for the specified account. | Any | Any |
| !account unban [email] | Unbans the specified account. | Moderator | Any |
@ -24,9 +24,10 @@ Account management commands.
## Achievement
Commands related to the achievement system.
| Command | Description | User Level | Invoker Type |
| ---------------------- | ------------------------------------------- | ---------- | ------------ |
| !achievement info [id] | Outputs info for the specified achievement. | Admin | Any |
| Command | Description | User Level | Invoker Type |
| ---------------------- | ------------------------------------------------------- | ---------- | ------------ |
| !achievement info [id] | Outputs info for the specified achievement. | Admin | Any |
| !achievement localeid | Generates a LocaleStringId from the specified argument. | Admin | Any |
## AOI
Commands for interacting with the invoker player's area of interest (AOI).
@ -44,6 +45,8 @@ Commands for boosting the stats of the invoker player's current avatar.
| Command | Description | User Level | Invoker Type |
| ----------------------- | --------------------------------------------------- | ---------- | ------------ |
| !boost damage [1-10000] | Sets DamagePctBonus for the current avatar. | Admin | Client |
| !boost invulnerable | Switches Invulnerable for the current avatar. | Admin | Client |
| !boost mana | Switches NoEnduranceCosts for the current avatar. | Admin | Client |
| !boost vsboss [1-10000] | Sets DamagePctBonusVsBosses for the current avatar. | Admin | Client |
## Client
@ -105,7 +108,7 @@ Commands for managing items.
| Command | Description | User Level | Invoker Type |
| ---------------------------- | -------------------------------------------------------------------------- | ---------- | ------------ |
| !item cleardeliverybox | Destroys all items contained in the delivery box inventory. | Any | Client |
| !item creditchest | Converts 500k credits to a sellable chest item. | Any | Client |
| !item creditchest | Converts credits to a sellable chest item. | Any | Client |
| !item destroyindestructible | Destroys indestructible items contained in the player's general inventory. | Any | Client |
| !item drop [pattern] [count] | Creates and drops the specified item from the current avatar. | Admin | Client |
| !item give [pattern] [count] | Creates and gives the specified item to the current player. | Admin | Client |
@ -208,20 +211,33 @@ Region management commands.
## Server
Server management commands.
| Command | Description | User Level | Invoker Type |
| ------------------------ | ----------------------------------------- | ---------- | ------------- |
| !server broadcast | Broadcasts a notification to all players. | Admin | Any |
| !server reloadlivetuning | Reloads live tuning settings. | Admin | ServerConsole |
| !server shutdown | Shuts the server down. | Admin | Any |
| !server status | Prints server status. | Any | Any |
| Command | Description | User Level | Invoker Type |
| --------------------------------- | ------------------------------------------------- | ---------- | ------------- |
| !server broadcast | Broadcasts a notification to all players. | Admin | Any |
| !server reloadaddg | Reloads the Add G page. | Admin | ServerConsole |
| !server reloadcatalog | Reloads MTX store catalog. | Admin | ServerConsole |
| !server reloaddashboard | Reloads the web dashboard. | Admin | ServerConsole |
| !server reloadlivetuning | Reloads live tuning settings. | Admin | ServerConsole |
| !server reloadplayernameblacklist | Reloads the player name blacklist. | Admin | ServerConsole |
| !server shutdown | Shuts the server down. | Admin | Any |
| !server status | Prints server status. | Any | Any |
| !server whitelist | Enables or disables account whitelist for logins. | Admin | ServerConsole |
## Store
Commands for interacting with the in-game store.
| Command | Description | User Level | Invoker Type |
| -------------------- | --------------------------------------------------------------- | ---------- | ------------ |
| !store addg [amount] | Adds the specified number of Gs to this account. | Admin | Client |
| !store convertes | Converts 100 Eternity Splinters to the equivalent amount of Gs. | Any | Client |
| Command | Description | User Level | Invoker Type |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------- | ------------ |
| !store addg [amount] | Adds the specified number of Gs to this account. | Admin | Client |
| !store convertes [amount] | Converts Eternity Splinters to the equivalent amount of Gs. Defaults to 100 Eternity Splinters if no value is specified. | Any | Client |
## UltimatePrestige
Ultimate Prestige system commands.
| Command | Description | User Level | Invoker Type |
| -------------------------- | ----------------------------------------------------- | ---------- | ------------ |
| !ultimateprestige activate | Activates the Ultimate Prestige for the current hero. | Any | Client |
| !ultimateprestige level | Prints the current Ultimate Prestige level. | Any | Client |
## Unlock
Commands for unlocking various things.
@ -231,6 +247,14 @@ Commands for unlocking various things.
| !unlock chapters | Unlocks all chapters. | Admin | Client |
| !unlock waypoints | Unlocks all waypoints. | Admin | Client |
## WebApi
Web API management commands.
| Command | Description | User Level | Invoker Type |
| ------------------- | ---------------------------- | ---------- | ------------- |
| !webapi generatekey | Generates a new web API key. | Admin | ServerConsole |
| !webapi reloadkeys | Reloads web API keys | Admin | ServerConsole |
## Misc
| Command | Description | User Level | Invoker Type |
@ -241,6 +265,7 @@ Commands for unlocking various things.
| !help | Help needs no help. | Any | Any |
| !jail | Teleports to East Side: Detention Facility (old). | Admin | Client |
| !position | Shows current position. | Any | Client |
| !syncmana | Syncs the current mana value with the server. | Any | Client |
| !tower | Teleports to Avengers Tower (original). | Any | Client |
| !tp | Teleports to position. Usage: tp x:+1000 (relative to current position) tp x100 y500 z10 (absolute position) | Admin | Client |

239
docs/ServerEmu/WebApi.md Normal file
View file

@ -0,0 +1,239 @@
# Web API
MHServerEmu can provide web API functionality as part of its web frontend. It is enabled by default, and the server listens for requests on `http://localhost:8080/`. This can be customized in the `WebFrontend` section of `Config.ini`.
The web API uses JSON for serialization. Requests return JSON as output and expect JSON as input in the request's body when needed.
Some endpoints are restricted and require an API key to access.
- API keys can be generated using the `!webapi generatekey` command.
- API keys are saved to `Data/Web/ApiKeys.json`. Keys can be reloaded from this file at runtime using the `!webapi reloadkeys` command.
- API keys are not logged, the only way to access generated keys is by reading them from the `ApiKeys.json` file.
- Each API key has a specific access type assigned to it (e.g. `AccountManagement`), which limits its functionality to a specific domain.
- API keys need to be provided when making requests to restricted endpoints by adding them as an `Authorization` HTTP request header with the `Bearer` scheme (e.g. `Authorization: Bearer FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF`).
Below is reference information on available endpoints.
## Account Management
### /AccountManagement/Create
#### POST
Requests the server to create an account with the provided data.
- Access: `None`
- Input Fields:
- `string Email`
- `string PlayerName`
- `string Password`
- Output Fields:
- `int Result`
### /AccountManagement/SetPlayerName
#### POST
Requests the server to change the player name of an existing account.
- Access: `AccountManagement`
- Input Fields:
- `string Email`
- `string PlayerName`
- Output Fields:
- `int Result`
### /AccountManagement/SetPassword
#### POST
Requests the server to change the password of an existing account.
- Access: `AccountManagement`
- Input Fields:
- `string Email`
- `string Password`
- Output Fields:
- `int Result`
### /AccountManagement/SetUserLevel
#### POST
Requests the server to change the user level of an existing account.
- Access: `AccountManagement`
- Input Fields:
- `string Email`
- `byte UserLevel`
- Output Fields:
- `int Result`
### /AccountManagement/SetFlag
#### POST
Requests the server to set a flag on an existing account.
- Access: `AccountManagement`
- Input Fields:
- `string Email`
- `int Flags`
- Output Fields:
- `int Result`
Account Flag Reference:
```csharp
enum AccountFlags
{
None = 0,
IsBanned = 1 << 0,
IsArchived = 1 << 1,
IsPasswordExpired = 1 << 2,
DEPRECATEDLinuxCompatibilityMode = 1 << 3,
IsWhitelisted = 1 << 4,
}
```
### /AccountManagement/ClearFlag
#### POST
Requests the server to clear a flag on an existing account.
- Access: `AccountManagement`
- Input Fields:
- `string Email`
- `int Flags`
- Output Fields:
- `int Result`
See `/AccountManagement/SetFlag` above for flag reference.
## Server Status
### /ServerStatus
#### GET
Retrieves server status information.
- Access: `None`
- Output:
- `Dictionary<string, long>`
## Region Report
### /RegionReport
#### GET
Retrieves server region information.
- Access: `None`
- Output Fields:
- `List<RegionReport.Entry> Regions`
- `string GameId`
- `string RegionId`
- `string Name`
- `string DifficultyTier`
- `string Uptime`
## Metrics
### /Metrics/Performance
#### GET
Retrieves server performance metrics.
- Access: `None`
- Output Fields:
- `string Id`
- `MemoryMetrics.Report Memory`
- `long GCIndex`
- `long GCCountGen0`
- `long GCCountGen1`
- `long TotalCommittedBytes`
- `long HeapSizeBytes`
- `double PauseTimePercentage`
- `MetricTracker.ReportEntry PauseDuration`
- `Dictionary<ulong, GamePerformanceMetrics.Report> Games`
- `MetricTracker.ReportEntry UpdateTime`
- `MetricTracker.ReportEntry FrameTime`
- `MetricTracker.ReportEntry ScheduledEventsPerUpdate`
- `MetricTracker.ReportEntry EntityCount`
- `MetricTracker.ReportEntry PlayerCount`
- `MetricTracker.ReportEntry` fields:
- `float Average`
- `float Median`
- `float Last`
- `float Min`
- `float Max`

View file

@ -41,16 +41,30 @@ Setting the server up for connections outside of your local network requires the
## Managing Accounts
You can create and manage accounts by using ! commands in the server console or the in-game chat window. Here are some commands to get you started:
You can create and manage accounts by using `!` commands in the server console or the in-game chat window. Here are some commands to get you started:
- `!account create [email] [playerName] [password]` - creates a new account with the specified email, player name, and password. Email and player name must be unique for each account.
- `!account userlevel [0|1|2]` - sets user level for the specified account to user (0), moderator (1), or admin (2). Higher user levels enable additional in-game command privileges, up to being able to manage other accounts and shut down the server.
- `!account userlevel [email] [0|1|2]` - sets user level for the specified account to user (0), moderator (1), or admin (2). Higher user levels enable additional in-game command privileges, up to being able to manage other accounts and shut down the server.
- `!account password [email] [newPassword]` - changes password for the specified account.
For a more in-depth list of commands see [Server Commands](./../ServerEmu/ServerCommands.md) or type `!commands`.
## Enabling Server Garbage Collection
When hosting a server for larger player counts (50+), it is recommended to enable .NET's server garbage collection mode.
The easiest way to enable it for MHServerEmu is to modify the `MHServerEmu.runtimeconfig.json` file located next to `MHServerEmu.exe`:
1. Open `MHServerEmu.runtimeconfig.json` with a text editor.
2. Add the following line to the `configProperties` section: `"System.GC.Server": true`.
Please keep in mind that the server garbage collection mode tends to follow the "any RAM not used is RAM wasted" approach, which can result in very heavy RAM usage, especially when the server stays up for longer periods of time. You may want to limit memory usage by adding the following line to the same `configProperties` section of `MHServerEmu.runtimeconfig.json`: `"System.GC.HeapHardLimitPercent": 80` (this will limit usage to 80% of available RAM).
You can find out more about the differences between the workstation (default) and the server modes in the [.NET documentation](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/workstation-server-gc).
## Setting Up Live Tips
The client can download additional loading screen tips from the server.

View file

@ -16,7 +16,7 @@ The following instructions are intended for stable builds of the server. If you
1. Run `StartServer.bat` and wait for MHServerEmu to initialize. There should be two server windows, one of them should be minimized by default and blank.
2. (Optional) Open http://localhost:8080/AccountManagement/Create and create an account. Please note that this link is going to work only when the server is running.
2. (Optional) Open [http://localhost:8080/Dashboard/](http://localhost:8080/Dashboard/) and create an account. Please note that this link is going to work only when the server is running.
3. Run `StartClient.bat` and log in with your created account OR run `StartClientAutoLogin.bat` to play with a default account.

View file

@ -31,7 +31,7 @@ Now you can actually start everything and get in-game.
2. Start MHServerEmu and wait for it to load.
3. Open the following link in your web browser and create your account: [http://localhost:8080/AccountManagement/Create](http://localhost:8080/AccountManagement/Create). This link is going to work only when MHServerEmu is fully up and running.
3. Open the following link in your web browser and create your account: [http://localhost:8080/Dashboard/](http://localhost:8080/Dashboard/). This link is going to work only when MHServerEmu is fully up and running.
4. Launch the game with the following argument: `-siteconfigurl=localhost/SiteConfig.xml`.

View file

@ -1,6 +1,19 @@
# Embedded Browser
Some of the in-game UI panels (TOS popup on login, store, community news) use a CEF-based web browser that can load HTML pages. These pages can interact with the UI via a JavaScript API.
Some of the in-game UI panels (TOS popup on login, store, community news) use an embedded web browser that can load HTML pages. These pages can interact with the client UI via a JavaScript API.
## Browser Versions
Originally the client used Awesomium as the browser backend, but it was replaced with Chromium Embedded Framework (CEF) in game version 1.22. Below is a list of known browser versions used by the client.
| Game Version | Browser Version | Browser Release Date |
| ------------ | ------------------------ | -------------------- |
| 1.9-1.21 | Awesomium 1.6.5 | 2012-02-23 |
| 1.22-1.32 | CEF 3.1650.1544 | 2013-12-08 |
| 1.33 | CEF 3.2272.2035 | 2015-02-26 |
| 1.34 | CEF 3.2272.2077 | 2015-04-13 |
| 1.35-1.52 | CEF 3.1650.1639 | 2014-03-13 |
| 1.53 | CEF 3.3112.1656.g9ec3e42 | 2017-08-10 |
## API Calls
@ -50,3 +63,11 @@ The actual viewable area is slightly smaller than these.
- Community News (Version 2) Main Page 988x644
- Community News (Version 2) Popup: 650x764
## Bundles
- Bundle images are downloaded and cached in `%TEMP%\MarvelHeroes`. It is possible for the client to cache an invalid bundle image, which may require clearing the cache to fix it.
- A bundle image needs to be a PNG file with its horizontal and vertical resolution being a multiple of 4. Preferred resolution is 344x128.
- `?gmode=` is appended to information page requests, indicating whether the gifting mode is enabled (0 or 1).

View file

@ -1,11 +0,0 @@
using MHServerEmu.Core.Config;
namespace MHServerEmu.Auth
{
public class AuthConfig : ConfigContainer
{
public string Address { get; private set; } = "localhost";
public string Port { get; private set; } = "8080";
public bool EnableWebApi { get; private set; } = true;
}
}

View file

@ -1,159 +0,0 @@
using System.Net;
using MHServerEmu.Auth.Handlers;
using MHServerEmu.Core.Config;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Network;
namespace MHServerEmu.Auth
{
/// <summary>
/// Handles HTTP requests from clients.
/// </summary>
public class AuthServer : IGameService
{
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly string _url;
private readonly AuthProtobufHandler _protobufHandler;
private readonly AuthWebApiHandler _webApiHandler;
private CancellationTokenSource _cts;
private HttpListener _listener;
public GameServiceState State { get; private set; } = GameServiceState.Created;
/// <summary>
/// Constructs a new <see cref="AuthServer"/> instance.
/// </summary>
public AuthServer()
{
var config = ConfigManager.Instance.GetConfig<AuthConfig>();
_url = $"http://{config.Address}:{config.Port}/";
_protobufHandler = new();
if (config.EnableWebApi)
_webApiHandler = new();
}
#region IGameService Implementation
/// <summary>
/// Runs this <see cref="AuthServer"/> instance.
/// </summary>
public async void Run()
{
// Reset CTS
_cts?.Dispose();
_cts = new();
// Create an http server and start listening for incoming connections
_listener = new HttpListener();
_listener.Prefixes.Add(_url);
_listener.Start();
Logger.Info($"AuthServer is listening on {_url}...");
State = GameServiceState.Running;
while (true)
{
try
{
// Wait for a connection, and handle the request
HttpListenerContext context = await _listener.GetContextAsync().WaitAsync(_cts.Token);
await HandleRequestAsync(context.Request, context.Response);
context.Response.Close();
}
catch (TaskCanceledException) { return; } // Stop handling connections
catch (Exception e)
{
Logger.Error($"Run(): Unhandled exception: {e}");
}
}
}
/// <summary>
/// Stops listening and shuts down this <see cref="AuthServer"/> instance.
/// </summary>
public void Shutdown()
{
if (_listener == null) return;
if (_listener.IsListening == false) return;
// Cancel async tasks (listening for context)
_cts.Cancel();
// Close the listener
_listener.Close();
_listener = null;
State = GameServiceState.Shutdown;
}
public void ReceiveServiceMessage<T>(in T message) where T : struct, IGameServiceMessage
{
// AuthServer should not be handling messages from TCP clients
switch (message)
{
default:
Logger.Warn($"ReceiveServiceMessage(): Unhandled service message type {typeof(T).Name}");
break;
}
}
public string GetStatus()
{
if (_listener == null || _listener.IsListening == false)
return "Not listening";
return $"Protobuf Handler: {_protobufHandler != null} | Web API Handler: {_webApiHandler != null}";
}
#endregion
/// <summary>
/// Routes an <see cref="HttpListenerRequest"/> to the appropriate handler.
/// </summary>
private async Task HandleRequestAsync(HttpListenerRequest request, HttpListenerResponse response)
{
bool requestIsFromGameClient = (request.UserAgent == "Secret Identity Studios Http Client");
// We should be getting only GET and POST
switch (request.HttpMethod)
{
case "GET":
if (request.Url.LocalPath == "/favicon.ico") return; // Ignore favicon requests
// Web API get requests
if (requestIsFromGameClient == false && _webApiHandler != null)
{
await _webApiHandler.HandleRequestAsync(request, response);
return;
}
break;
case "POST":
// Client auth messages
if (requestIsFromGameClient && request.Url.LocalPath == "/Login/IndexPB")
{
await _protobufHandler.HandleMessageAsync(request, response);
return;
}
// Web API post requests
if (requestIsFromGameClient == false && _webApiHandler != null)
{
await _webApiHandler.HandleRequestAsync(request, response);
return;
}
break;
}
// Display a warning for unhandled requests
string source = requestIsFromGameClient ? "a game client" : $"an unknown UserAgent ({request.UserAgent})";
Logger.Warn($"HandleRequestAsync(): Unhandled {request.HttpMethod} to {request.Url.LocalPath} from {source} on {request.RemoteEndPoint}");
}
}
}

View file

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Account</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
<style>
html {
background-color: #0D0D0B;
}
div.content {
max-width: 800px;
margin-left: auto;
margin-right: auto;
border: 1px solid #00717c;
background-color: #00090E;
padding: 20px;
}
div.content h1, h2, h3, h4, h5, h6 {
color: #00aaff;
font-family: "Bebas Neue", "Trebuchet MS", Verdana, sans-serif;
font-weight: normal;
}
div.content p {
color: #d7d7d7;
font: 12px/1.385 Verdana;
}
div.content p.disclaimer {
color: red;
}
div.content input {
border: 1px solid black;
background: rgb(28, 28, 28);
box-shadow: 0 0 1px 1px #00aaff;
color: #d7d7d7;
font: 12px/1.385 Verdana;
}
div.content label {
color: #d7d7d7;
font-family: "Bebas Neue", "Trebuchet MS", Verdana, sans-serif;
font-size: x-large;
font-weight: normal;
}
</style>
</head>
<body>
<div class="content">
<h1>Create Account</h1>
<p class="disclaimer">
MHServerEmu is alpha software in active development, and it may not be completely secure.</br>
Please avoid using your real email / password combinations on public servers.
</p>
<form method="POST">
<label for="email">Email</label><br>
<input type="text" id="email" name="email" maxlength="320" required><br><br>
<label for="playerName">Player Name</label><br>
<input type="text" id="playerName" name="playerName" maxlength="16" required><br><br>
<label for="password">Password</label><br>
<input type="password" id="password" name="password" minlength="3" maxlength="64" required><br><br>
<input type="submit" value="Submit">
</form>
</div>
</body>
</html>

View file

@ -1,58 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%RESPONSE_TITLE%</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" rel="stylesheet">
<style>
html {
background-color: #0D0D0B;
}
div.content {
max-width: 800px;
margin-left: auto;
margin-right: auto;
border: 1px solid #00717c;
background-color: #00090E;
padding: 20px;
}
div.content h1, h2, h3, h4, h5, h6 {
color: #00aaff;
font-family: "Bebas Neue", "Trebuchet MS", Verdana, sans-serif;
font-weight: normal;
}
div.content p {
color: #d7d7d7;
font: 12px/1.385 Verdana;
}
li {
color: #d7d7d7;
font: 12px/1.385 Verdana;
}
table {
overflow-x: auto;
display: block;
}
td {
color: #d7d7d7;
font: 12px/1.385 Verdana;
padding: 4px;
}
</style>
</head>
<body>
<div class="content">
<h1>%RESPONSE_TITLE%</h1>
<p>%RESPONSE_TEXT%</p>
</div>
</body>
</html>

View file

@ -1,96 +0,0 @@
using System.Net;
using Gazillion;
using Google.ProtocolBuffers;
using MHServerEmu.Core.Config;
using MHServerEmu.Core.Extensions;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Network;
using MHServerEmu.PlayerManagement;
using MHServerEmu.PlayerManagement.Network;
namespace MHServerEmu.Auth.Handlers
{
/// <summary>
/// Handler for <see cref="IMessage"/> instances sent to the <see cref="AuthServer"/>.
/// </summary>
public class AuthProtobufHandler
{
private static readonly Logger Logger = LogManager.CreateLogger();
private static readonly bool HideSensitiveInformation = ConfigManager.Instance.GetConfig<LoggingConfig>().HideSensitiveInformation;
/// <summary>
/// Receives and handles an <see cref="IMessage"/>.
/// </summary>
public async Task HandleMessageAsync(HttpListenerRequest request, HttpListenerResponse response)
{
MessageBuffer messageBuffer = new(request.InputStream);
switch ((FrontendProtocolMessage)messageBuffer.MessageId)
{
case FrontendProtocolMessage.LoginDataPB: await OnLoginDataPB(request, response, messageBuffer); break;
case FrontendProtocolMessage.PrecacheHeaders: await OnPrecacheHeaders(request, response, messageBuffer); break;
default: Logger.Warn($"HandleMessageAsync(): Unhandled {(FrontendProtocolMessage)messageBuffer.MessageId} [{messageBuffer.MessageId}]"); break;
}
}
/// <summary>
/// Handles a <see cref="LoginDataPB"/> message.
/// </summary>
private async Task<bool> OnLoginDataPB(HttpListenerRequest httpRequest, HttpListenerResponse httpResponse, MessageBuffer messageBuffer)
{
LoginDataPB loginDataPB = messageBuffer.Deserialize<FrontendProtocolMessage>() as LoginDataPB;
if (loginDataPB == null) return Logger.WarnReturn(false, $"OnLoginDataPB(): Failed to retrieve message");
// Mask the end point name to prevent sensitive information from appearing in logs in needed
string endPointName = HideSensitiveInformation
? httpRequest.RemoteEndPoint.ToStringMasked()
: httpRequest.RemoteEndPoint.ToString();
#if DEBUG
// Send a TOS popup when the client uses tos@test.com as email
if (loginDataPB.EmailAddress == "tos@test.com")
{
var tosTicket = AuthTicket.CreateBuilder()
.SetSessionId(0)
.SetTosurl("http://localhost/tos") // The client adds &locale=en_us to this url (or another locale code)
.Build();
await HttpHelper.SendProtobufAsync(httpResponse, tosTicket, (int)AuthStatusCode.NeedToAcceptLegal);
return true;
}
#endif
// Try to create a new session from the data we received
PlayerManagerService playerManager = ServerManager.Instance.GetGameService(GameServiceType.PlayerManager) as PlayerManagerService;
if (playerManager == null)
return Logger.ErrorReturn(false, $"OnLoginDataPB(): Failed to connect to the player manager");
AuthStatusCode statusCode = playerManager.OnLoginDataPB(loginDataPB, out AuthTicket ticket);
// Respond with an error if session creation didn't succeed
if (statusCode != AuthStatusCode.Success)
{
httpResponse.StatusCode = (int)statusCode;
return Logger.InfoReturn(true, $"Authentication for the game client on {endPointName} failed ({statusCode})");
}
// Send an AuthTicket if we were able to create a session
string machineId = loginDataPB.HasMachineId ? loginDataPB.MachineId : string.Empty;
Logger.Info($"Sending AuthTicket for SessionId 0x{ticket.SessionId:X} to the game client on {endPointName}, machineId={machineId}");
await HttpHelper.SendProtobufAsync(httpResponse, ticket);
return true;
}
/// <summary>
/// Handles a <see cref="PrecacheHeaders"/> message.
/// </summary>
private async Task<bool> OnPrecacheHeaders(HttpListenerRequest httpRequest, HttpListenerResponse httpResponse, MessageBuffer messageBuffer)
{
// The client sends this message on startup
Logger.Trace($"Received PrecacheHeaders message");
await HttpHelper.SendProtobufAsync(httpResponse, PrecacheHeadersMessageResponse.DefaultInstance);
return true;
}
}
}

View file

@ -1,213 +0,0 @@
using System.Collections.Specialized;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Web;
using MHServerEmu.Core.Config;
using MHServerEmu.Core.Extensions;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Metrics;
using MHServerEmu.Core.Network;
using MHServerEmu.PlayerManagement;
using MHServerEmu.PlayerManagement.Players;
using MHServerEmu.PlayerManagement.Regions;
namespace MHServerEmu.Auth.Handlers
{
public enum AuthWebApiOutputFormat
{
Html,
Json
}
/// <summary>
/// Handler for web API requests sent to the <see cref="AuthServer"/>.
/// </summary>
public class AuthWebApiHandler
{
private static readonly Logger Logger = LogManager.CreateLogger();
private static readonly bool HideSensitiveInformation = ConfigManager.Instance.GetConfig<LoggingConfig>().HideSensitiveInformation;
private readonly string ResponseHtml;
private readonly string AccountCreateFormHtml;
/// <summary>
/// Constructs a new <see cref="AuthWebApiHandler"/> instance.
/// </summary>
public AuthWebApiHandler()
{
string assetDirectory = Path.Combine(FileHelper.DataDirectory, "Auth");
ResponseHtml = File.ReadAllText(Path.Combine(assetDirectory, "Response.html"));
AccountCreateFormHtml = File.ReadAllText(Path.Combine(assetDirectory, "AccountCreateForm.html"));
}
/// <summary>
/// Receives and handles a web API request.
/// </summary>
public async Task HandleRequestAsync(HttpListenerRequest httpRequest, HttpListenerResponse httpResponse)
{
// Mask end point name if needed
string endPointName = HideSensitiveInformation
? httpRequest.RemoteEndPoint.ToStringMasked()
: httpRequest.RemoteEndPoint.ToString();
if (Enum.TryParse(httpRequest.QueryString["outputFormat"], true, out AuthWebApiOutputFormat outputFormat) == false)
outputFormat = AuthWebApiOutputFormat.Html;
// Parse query string body from POST requests
NameValueCollection bodyQueryString = null;
if (httpRequest.HttpMethod == "POST")
{
using (StreamReader reader = new(httpRequest.InputStream))
bodyQueryString = HttpUtility.ParseQueryString(reader.ReadToEnd());
}
// Handling
switch (httpRequest.Url.LocalPath)
{
case "/AccountManagement/Create": await OnAccountCreate(bodyQueryString, httpResponse, outputFormat); break;
case "/ServerStatus": await OnServerStatus(httpResponse, outputFormat); break;
case "/RegionReport": await OnRegionReport(httpResponse, outputFormat); break;
case "/Metrics/Performance": await OnMetricsPerformance(httpResponse, outputFormat); break;
default:
Logger.Warn($"HandleRequestAsync(): Unhandled web API request\nRequest: {httpRequest.Url.LocalPath}\nRemoteEndPoint: {endPointName}\nUserAgent: {httpRequest.UserAgent}");
break;
}
}
/// <summary>
/// Sends <see cref="ResponseData"/> as an <see cref="HttpListenerResponse"/> using the specified <see cref="AuthWebApiOutputFormat"/>.
/// </summary>
private async Task SendResponseAsync(ResponseData responseData, HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
{
if (outputFormat == AuthWebApiOutputFormat.Html)
await HttpHelper.SendHtmlAsync(httpResponse, FormatResponseDataHtml(responseData));
else if (outputFormat == AuthWebApiOutputFormat.Json)
await HttpHelper.SendPlainTextAsync(httpResponse, JsonSerializer.Serialize(responseData));
else
Logger.Warn($"SendResponseAsync(): Unsupported output format {outputFormat}");
}
/// <summary>
/// Formats <see cref="ResponseData"> as an html page.
/// </summary>
private string FormatResponseDataHtml(ResponseData responseData)
{
StringBuilder sb = new(ResponseHtml);
sb.Replace("%RESPONSE_TITLE%", responseData.Title);
sb.Replace("%RESPONSE_TEXT%", responseData.Text);
return sb.ToString();
}
#region Request Handling
/// <summary>
/// Handles an account creation web request.
/// </summary>
private async Task<bool> OnAccountCreate(NameValueCollection bodyQueryString, HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
{
// Show account creation form when no parameters are specified in the query string
if (bodyQueryString == null)
{
if (outputFormat == AuthWebApiOutputFormat.Html)
await HttpHelper.SendHtmlAsync(httpResponse, AccountCreateFormHtml);
else if (outputFormat == AuthWebApiOutputFormat.Json)
await SendResponseAsync(new(false, "Invalid Request", "This request does not support JSON output."), httpResponse, outputFormat);
return true;
}
// Validate input
bool inputIsValid = true;
inputIsValid &= string.IsNullOrWhiteSpace(bodyQueryString["email"]) == false;
inputIsValid &= string.IsNullOrWhiteSpace(bodyQueryString["playerName"]) == false;
inputIsValid &= string.IsNullOrWhiteSpace(bodyQueryString["password"]) == false;
if (inputIsValid == false)
{
await SendResponseAsync(new(false, "Error", "Input is not valid."), httpResponse, outputFormat);
return false;
}
(bool result, string text) = AccountManager.CreateAccount(bodyQueryString["email"].ToLower(), bodyQueryString["playerName"], bodyQueryString["password"]);
if (HideSensitiveInformation == false) Logger.Trace(text);
ResponseData responseData = new(result, result ? "Success" : "Error", text);
await SendResponseAsync(responseData, httpResponse, outputFormat);
return true;
}
/// <summary>
/// Handles a server status web request.
/// </summary>
private async Task<bool> OnServerStatus(HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
{
string serverStatus = ServerManager.Instance.GetServerStatus(false);
// Fix line breaks for display in browsers
if (outputFormat == AuthWebApiOutputFormat.Html)
serverStatus = serverStatus.Replace("\n", "<br/>");
await SendResponseAsync(new(true, "Server Status", serverStatus), httpResponse, outputFormat);
return true;
}
private async Task<bool> OnRegionReport(HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
{
if (ServerManager.Instance.GetGameService(GameServiceType.PlayerManager) is not PlayerManagerService playerManager)
return false;
using RegionReport regionReport = new();
playerManager.GetRegionReportData(regionReport);
if (outputFormat == AuthWebApiOutputFormat.Html)
{
StringBuilder sb = new();
HtmlBuilder.AppendDataStructure(sb, regionReport);
await SendResponseAsync(new(true, "Region Report", sb.ToString()), httpResponse, outputFormat);
}
else if (outputFormat == AuthWebApiOutputFormat.Json)
{
string json = JsonSerializer.Serialize(regionReport);
await HttpHelper.SendPlainTextAsync(httpResponse, json);
}
return true;
}
private async Task<bool> OnMetricsPerformance(HttpListenerResponse httpResponse, AuthWebApiOutputFormat outputFormat)
{
if (outputFormat == AuthWebApiOutputFormat.Html)
{
string report = MetricsManager.Instance.GeneratePerformanceReport(MetricsReportFormat.Html);
await SendResponseAsync(new(true, "Performance Report", report), httpResponse, outputFormat);
}
else if (outputFormat == AuthWebApiOutputFormat.Json)
{
string report = MetricsManager.Instance.GeneratePerformanceReport(MetricsReportFormat.Json);
await HttpHelper.SendPlainTextAsync(httpResponse, report);
}
return true;
}
#endregion
private readonly struct ResponseData
{
public bool Result { get; }
public string Title { get; }
public string Text { get; }
public ResponseData(bool result, string title, string text)
{
Result = result;
Title = title;
Text = text;
}
}
}
}

View file

@ -1,57 +0,0 @@
using System.Net;
using System.Text;
using Google.ProtocolBuffers;
using MHServerEmu.Core.Network;
namespace MHServerEmu.Auth
{
public static class HttpHelper
{
/// <summary>
/// Sends a string as a text/plain <see cref="HttpListenerResponse"/>.
/// </summary>
public static async Task SendPlainTextAsync(HttpListenerResponse httpResponse, string text, int statusCode = 200)
{
await SendTextAsync(httpResponse, text, "text/plain", statusCode);
}
/// <summary>
/// Sends a string as a text/html <see cref="HttpListenerResponse"/>.
/// </summary>
public static async Task SendHtmlAsync(HttpListenerResponse httpResponse, string text, int statusCode = 200)
{
await SendTextAsync(httpResponse, text, "text/html", statusCode);
}
/// <summary>
/// Sends an <see cref="IMessage"/> instance as an <see cref="HttpListenerResponse"/>.
/// </summary>
public static async Task SendProtobufAsync(HttpListenerResponse httpResponse, IMessage message, int statusCode = 200)
{
MessagePackageOut messagePackage = new(message);
httpResponse.StatusCode = statusCode;
httpResponse.KeepAlive = false;
httpResponse.ContentType = "application/octet-stream";
httpResponse.ContentLength64 = messagePackage.GetSerializedSize();
CodedOutputStream cos = CodedOutputStream.CreateInstance(httpResponse.OutputStream);
await Task.Run(() => { messagePackage.WriteTo(cos); cos.Flush(); });
}
/// <summary>
/// Sends a string as an <see cref="HttpListenerResponse"/>.
/// </summary>
private static async Task SendTextAsync(HttpListenerResponse httpResponse, string text, string contentType, int statusCode = 200)
{
byte[] buffer = Encoding.UTF8.GetBytes(text);
httpResponse.StatusCode = statusCode;
httpResponse.KeepAlive = false;
httpResponse.ContentType = contentType;
httpResponse.ContentLength64 = buffer.Length;
await httpResponse.OutputStream.WriteAsync(buffer);
}
}
}

View file

@ -1,46 +0,0 @@
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Network;
namespace MHServerEmu.Billing
{
public class BillingService : IGameService
{
// All the message handling / order fulfillment functionality has been moved to Games.MTXStore.CatalogManager.
// I'm keeping this service stub because eventually it will be used for converting ES to G via the store interface.
private static readonly Logger Logger = LogManager.CreateLogger();
public GameServiceState State { get; private set; } = GameServiceState.Created;
public BillingService() { }
#region IGameService Implementation
public void Run()
{
State = GameServiceState.Running;
}
public void Shutdown()
{
State = GameServiceState.Shutdown;
}
public void ReceiveServiceMessage<T>(in T message) where T : struct, IGameServiceMessage
{
switch (message)
{
default:
Logger.Warn($"ReceiveServiceMessage(): Unhandled service message type {typeof(T).Name}");
break;
}
}
public string GetStatus()
{
return "Running";
}
#endregion
}
}

View file

@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<Platforms>x64</Platforms>
</PropertyGroup>
<PropertyGroup>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MHServerEmu.Core\MHServerEmu.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Google.ProtocolBuffers">
<HintPath>..\..\dep\protobuf-csharp\Google.ProtocolBuffers.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,100 @@
namespace MHServerEmu.Core.Collections
{
public class FixedDeque<T>
{
private readonly T[] _items;
private int _first;
private int _last;
private readonly int _maxSize;
public FixedDeque(int size)
{
_maxSize = size + 1;
_items = new T[_maxSize];
_first = 0;
_last = 0;
}
public int Capacity => _maxSize - 1;
public int Size => (_last - _first + _maxSize) % _maxSize;
public bool Empty => _first == _last;
public void Clear()
{
Array.Clear(_items, 0, _items.Length);
_first = _last = 0;
}
public T this[int index]
{
get
{
if (index >= Size)
throw new IndexOutOfRangeException();
return _items[(_first + index) % _maxSize];
}
set
{
if (index >= Size)
throw new IndexOutOfRangeException();
_items[(_first + index) % _maxSize] = value;
}
}
public T Front => _items[_first];
public T Back => _items[(_last + _maxSize - 1) % _maxSize];
public void PushBack(T value)
{
_items[_last] = value;
_last = (_last + 1) % _maxSize;
}
public void PushFront(T value)
{
_first = (_first + _maxSize - 1) % _maxSize;
_items[_first] = value;
}
public T PopBack()
{
_last = (_last + _maxSize - 1) % _maxSize;
T result = _items[_last];
_items[_last] = default;
return result;
}
public bool TryPopBack(out T value)
{
if (Empty)
{
value = default;
return false;
}
_last = (_last + _maxSize - 1) % _maxSize;
value = _items[_last];
_items[_last] = default;
return true;
}
public T PopFront()
{
T result = _items[_first];
_items[_first] = default;
_first = (_first + 1) % _maxSize;
return result;
}
public bool TryPopFront(out T value)
{
if (Empty)
{
value = default;
return false;
}
value = _items[_first];
_items[_first] = default;
_first = (_first + 1) % _maxSize;
return true;
}
}
}

View file

@ -1,8 +1,12 @@
namespace MHServerEmu.Core.Collections
{
public class FixedPriorityQueue<T> where T : IComparable<T>
public readonly struct FixedPriorityQueue<T> where T : IComparable<T>
{
// Our implementation is just a List<T> wrapper with no data of its own, so we can get away with making it a readonly struct.
// If this ever needs to stop being the case, turn it into a fully featured ICollection<T> implementation and pool it.
private readonly List<T> _items;
public bool Empty => _items.Count == 0;
public int Count => _items.Count;
public T Top => _items[0];
@ -12,6 +16,11 @@
_items = new(capacity);
}
public FixedPriorityQueue(List<T> items)
{
_items = items;
}
public void Push(T value)
{
_items.Add(value);
@ -27,7 +36,10 @@
_items.RemoveAt(_items.Count - 1);
}
public void Clear() => _items.Clear();
public void Clear()
{
_items.Clear();
}
public void Heapify()
{
@ -104,102 +116,4 @@
PushHeapIndex(list, holeIndex, topIndex, value);
}
}
public class FixedDeque<T>
{
private readonly T[] _items;
private int _first;
private int _last;
private readonly int _maxSize;
public FixedDeque(int size)
{
_maxSize = size + 1;
_items = new T[_maxSize];
_first = 0;
_last = 0;
}
public int Capacity => _maxSize - 1;
public int Size => (_last - _first + _maxSize) % _maxSize;
public bool Empty => _first == _last;
public void Clear()
{
Array.Clear(_items, 0, _items.Length);
_first = _last = 0;
}
public T this[int index]
{
get
{
if (index >= Size)
throw new IndexOutOfRangeException();
return _items[(_first + index) % _maxSize];
}
set
{
if (index >= Size)
throw new IndexOutOfRangeException();
_items[(_first + index) % _maxSize] = value;
}
}
public T Front => _items[_first];
public T Back => _items[(_last + _maxSize - 1) % _maxSize];
public void PushBack(T value)
{
_items[_last] = value;
_last = (_last + 1) % _maxSize;
}
public void PushFront(T value)
{
_first = (_first + _maxSize - 1) % _maxSize;
_items[_first] = value;
}
public T PopBack()
{
_last = (_last + _maxSize - 1) % _maxSize;
T result = _items[_last];
_items[_last] = default;
return result;
}
public bool TryPopBack(out T value)
{
if (Empty)
{
value = default;
return false;
}
_last = (_last + _maxSize - 1) % _maxSize;
value = _items[_last];
_items[_last] = default;
return true;
}
public T PopFront()
{
T result = _items[_first];
_items[_first] = default;
_first = (_first + 1) % _maxSize;
return result;
}
public bool TryPopFront(out T value)
{
if (Empty)
{
value = default;
return false;
}
value = _items[_first];
_items[_first] = default;
_first = (_first + 1) % _maxSize;
return true;
}
}
}

View file

@ -0,0 +1,49 @@
using System.Runtime.CompilerServices;
namespace MHServerEmu.Core.Collections
{
/// <summary>
/// Generic inline array of 2 elements of type <typeparamref name="T"/>.
/// </summary>
[InlineArray(2)]
public struct InlineArray2<T>
{
private T _element0;
}
/// <summary>
/// Generic inline array of 3 elements of type <typeparamref name="T"/>.
/// </summary>
[InlineArray(3)]
public struct InlineArray3<T>
{
private T _element0;
}
/// <summary>
/// Generic inline array of 4 elements of type <typeparamref name="T"/>.
/// </summary>
[InlineArray(4)]
public struct InlineArray4<T>
{
private T _element0;
}
/// <summary>
/// Generic inline array of 6 elements of type <typeparamref name="T"/>.
/// </summary>
[InlineArray(6)]
public struct InlineArray6<T>
{
private T _element0;
}
/// <summary>
/// Generic inline array of 8 elements of type <typeparamref name="T"/>.
/// </summary>
[InlineArray(8)]
public struct InlineArray8<T>
{
private T _element0;
}
}

View file

@ -1,58 +1,59 @@
using System.Collections;
using System.Runtime.CompilerServices;
namespace MHServerEmu.Core.Collections
{
public class InvasiveList<T>
{
private readonly Iterator[] _iterators;
private readonly Stack<Iterator> _iteratorPool;
private Iterator _reusableIterator;
private int _numIterators;
public int Id { get; private set; }
public T Head { get; set; }
public T Head { get; private set; }
public T Tail { get; private set; }
public int Count { get; private set; }
private Iterator[] _iterators;
private int _numIterators;
private int _maxIterators;
public bool IsEmpty { get => Head == null; }
public InvasiveList(int maxIterators)
public InvasiveList(int maxIterators, int id = 0)
{
_maxIterators = maxIterators;
_iterators = new Iterator[_maxIterators];
}
_iterators = new Iterator[maxIterators];
if (maxIterators > 1)
_iteratorPool = new();
public InvasiveList(int maxIterators, int id)
{
_maxIterators = maxIterators;
_iterators = new Iterator[_maxIterators];
Id = id;
}
public IEnumerable<T> Iterate()
public IEnumerator<T> GetEnumerator()
{
var iterator = new Iterator(this);
Iterator iterator;
try
if (_iteratorPool != null)
{
while (iterator.End() == false)
{
var element = iterator.Current;
iterator.MoveNext();
yield return element;
}
if (_iteratorPool.TryPop(out iterator) == false)
iterator = new(this);
}
finally
else
{
UnregisterIterator(iterator);
_reusableIterator ??= new(this);
iterator = _reusableIterator;
}
iterator.Initialize();
return iterator;
}
public bool IsEmpty() => Head == null;
public void Remove(T element)
{
if (element == null || Contains(element) == false) return;
var node = GetInvasiveListNode(element, Id);
if (node == null) return;
ref var node = ref GetInvasiveListNode(element, Id);
if (Unsafe.IsNullRef(ref node)) return;
for (int i = 0; i < _numIterators; i++)
{
@ -68,16 +69,16 @@ namespace MHServerEmu.Core.Collections
if (node.Next != null)
{
T nextElement = node.Next;
var nextNode = GetInvasiveListNode(nextElement, Id);
if (nextNode != null)
ref var nextNode = ref GetInvasiveListNode(nextElement, Id);
if (Unsafe.IsNullRef(ref nextNode) == false)
nextNode.Prev = node.Prev;
}
if (node.Prev != null)
{
T prevElement = node.Prev;
var prevNode = GetInvasiveListNode(prevElement, Id);
if (prevNode != null)
ref var prevNode = ref GetInvasiveListNode(prevElement, Id);
if (Unsafe.IsNullRef(ref prevNode) == false)
prevNode.Next = node.Next;
}
@ -93,11 +94,11 @@ namespace MHServerEmu.Core.Collections
if (oldElement == null || Contains(oldElement) == false) return;
if (element == null || Contains(element)) return;
var node = GetInvasiveListNode(element, Id);
if (node == null) return;
ref var node = ref GetInvasiveListNode(element, Id);
if (Unsafe.IsNullRef(ref node)) return;
var oldNode = GetInvasiveListNode(oldElement, Id);
if (oldNode == null) return;
ref var oldNode = ref GetInvasiveListNode(oldElement, Id);
if (Unsafe.IsNullRef(ref oldNode)) return;
var oldPrev = oldNode.Prev;
oldNode.Prev = element;
@ -106,8 +107,8 @@ namespace MHServerEmu.Core.Collections
if (oldPrev != null)
{
var oldPrevNode = GetInvasiveListNode(oldPrev, Id);
if (oldPrevNode == null) return;
ref var oldPrevNode = ref GetInvasiveListNode(oldPrev, Id);
if (Unsafe.IsNullRef(ref oldPrevNode)) return;
oldPrevNode.Next = element;
}
else
@ -120,14 +121,14 @@ namespace MHServerEmu.Core.Collections
{
if (element == null || Contains(element)) return;
var node = GetInvasiveListNode(element, Id);
if (node == null) return;
ref var node = ref GetInvasiveListNode(element, Id);
if (Unsafe.IsNullRef(ref node)) return;
node.Prev = Tail;
if (Tail != null)
{
var tailNode = GetInvasiveListNode(Tail, Id);
if (tailNode == null) return;
ref var tailNode = ref GetInvasiveListNode(Tail, Id);
if (Unsafe.IsNullRef(ref tailNode)) return;
tailNode.Next = element;
}
else
@ -137,20 +138,23 @@ namespace MHServerEmu.Core.Collections
Count++;
}
public virtual InvasiveListNode<T> GetInvasiveListNode(T element, int listId) => null;
public virtual ref InvasiveListNode<T> GetInvasiveListNode(T element, int listId)
{
return ref Unsafe.NullRef<InvasiveListNode<T>>();
}
public bool Contains(T element)
{
if (element == null) return false;
var node = GetInvasiveListNode(element, Id);
if (node == null) return false;
ref var node = ref GetInvasiveListNode(element, Id);
if (Unsafe.IsNullRef(ref node)) return false;
return node.Next != null || node.Prev != null || element.Equals(Head);
}
private void RegisterIterator(Iterator iterator)
{
if (_numIterators >= _maxIterators)
throw new InvalidOperationException($"Too many iterators '{_maxIterators}' for invasive list");
if (_numIterators >= _iterators.Length)
throw new InvalidOperationException($"Too many iterators '{_iterators.Length}' for invasive list");
_iterators[_numIterators++] = iterator;
}
@ -165,71 +169,75 @@ namespace MHServerEmu.Core.Collections
_iterators[_numIterators - 1] = null;
_numIterators--;
// pool iterator instance for reuse
iterator.Reset();
_iteratorPool?.Push(iterator);
return;
}
throw new InvalidOperationException("Iterator not found in iterator collection of invasive list!");
}
public class Iterator : IEnumerator<T>
public sealed class Iterator : IEnumerator<T>
{
private InvasiveList<T> _list;
public bool SkipNext { get; set; }
private readonly InvasiveList<T> _list;
private bool _start = true;
public T Current { get; private set; }
object IEnumerator.Current { get => Current; }
public bool SkipNext { get; set; } = false;
public Iterator(InvasiveList<T> invasiveList)
{
_list = invasiveList;
Current = _list.Head;
SkipNext = false;
}
public void Initialize()
{
_list.RegisterIterator(this);
}
public T Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose() { }
public void Reset() { }
public void Dispose()
{
_list.UnregisterIterator(this);
}
public void Reset()
{
_start = true;
Current = default;
SkipNext = false;
}
public bool MoveNext()
{
if (SkipNext) SkipNext = false;
else if (Current != null)
Current = _list.GetInvasiveListNode(Current, _list.Id).Next;
if (_start)
{
Current = _list.Head;
_start = false;
}
else
{
if (SkipNext)
SkipNext = false;
else if (Current != null)
Current = _list.GetInvasiveListNode(Current, _list.Id).Next;
}
return true;
return Current != null;
}
public bool End() => Current == null;
}
}
public class InvasiveListNode<T>
public struct InvasiveListNode<T>
{
public T Next { get; set; }
public T Prev { get; set; }
public T Next;
public T Prev;
public void Clear() => Next = Prev = default;
}
public class InvasiveListNodeCollection<T>
{
private readonly InvasiveListNode<T>[] _nodes;
private int _numLists;
public InvasiveListNodeCollection(int numLists)
{
_numLists = numLists;
_nodes = new InvasiveListNode<T>[_numLists];
for (int i = 0; i < _numLists; i++)
_nodes[i] = new();
}
public InvasiveListNode<T> GetInvasiveListNode(int listIndex)
{
if (listIndex >= 0 && listIndex < _numLists)
return _nodes[listIndex];
else
return null;
}
}
}

View file

@ -33,9 +33,7 @@ namespace MHServerEmu.Core.Collections
public Picker(Picker<T> other)
{
_elements = new(other._elements.Count);
foreach (WeightedElement element in other._elements)
_elements.Add(element);
_elements = new(other._elements);
// IMPORTANT: The copy needs to use the same instance of random as the original to preserve the RNG sequence,
// otherwise PickValidItem() and PickWeightTryAll() will keep picking the same things.
@ -188,15 +186,10 @@ namespace MHServerEmu.Core.Collections
_weights = 0;
}
private class WeightedElement
private readonly struct WeightedElement(T element, int weight)
{
public T Element { get; }
public int Weight { get; }
public WeightedElement(T element, int weight)
{
Element = element;
Weight = weight;
}
public T Element { get; } = element;
public int Weight { get; } = weight;
}
}
}

View file

@ -0,0 +1,22 @@
namespace MHServerEmu.Core.Collections
{
/// <summary>
/// A modification of <see cref="Stack{T}"/> that implements <see cref="ICollection{T}"/> for compatibility with our CollectionPool implementation.
/// </summary>
public sealed class PoolableStack<T> : Stack<T>, ICollection<T>
{
public bool IsReadOnly { get => false; }
[Obsolete("This method is not supported.")]
public void Add(T item)
{
throw new NotSupportedException();
}
[Obsolete("This method is not supported.")]
public bool Remove(T item)
{
throw new NotSupportedException();
}
}
}

View file

@ -70,15 +70,12 @@ namespace MHServerEmu.Core.Collisions
/// <summary>
/// Return the coordinates of the corners
/// </summary>
public Point2[] GetPoints()
public void GetPoints(Span<Point2> points)
{
return new Point2[]
{
new (Min.X, Min.Y),
new (Min.X, Max.Y),
new (Max.X, Max.Y),
new (Max.X, Min.Y)
};
points[0] = new(Min.X, Min.Y);
points[1] = new(Min.X, Max.Y);
points[2] = new(Max.X, Max.Y);
points[3] = new(Max.X, Min.Y);
}
public Aabb2 Translate(Vector2 newPosition) => new(Min + newPosition, Max + newPosition);

View file

@ -1,15 +1,15 @@
using System.Text;
using MHServerEmu.Core.Collections;
using MHServerEmu.Core.VectorMath;
namespace MHServerEmu.Core.Collisions
{
public struct Triangle
{
public Vector3[] Points;
public InlineArray3<Vector3> Points;
public Triangle(Vector3 p0, Vector3 p1, Vector3 p2)
{
Points = new Vector3[3];
Points[0] = p0;
Points[1] = p1;
Points[2] = p2;

View file

@ -106,6 +106,17 @@ namespace MHServerEmu.Core.Extensions
return sb.ToString();
}
public static bool IsAscii(this ReadOnlySpan<char> chars)
{
foreach (char c in chars)
{
if (char.IsAscii(c) == false)
return false;
}
return true;
}
#endregion
}
}

View file

@ -0,0 +1,44 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MHServerEmu.Core.Extensions
{
public static class DictionaryExtensions
{
/// <summary>
/// Retrieves a <see langword="ref"/> to a value corresponding to a <typeparamref name="TKey"/> key in this <see cref="Dictionary{TKey, TValue}"/>.
/// Returns <see langword="true"/> if a value was found.
/// </summary>
public static bool TryGetValueRef<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, ref TValue value)
{
value = ref CollectionsMarshal.GetValueRefOrNullRef(dictionary, key);
return Unsafe.IsNullRef(ref value) == false;
}
/// <summary>
/// Returns a <see langword="ref"/> to a value corresponding to a <typeparamref name="TKey"/> key in this <see cref="Dictionary{TKey, TValue}"/>.
/// Adds a <see langword="default"/> value if no existing value corresponds to the specified key.
/// </summary>
/// <remarks>
/// This has behavior similar to std::map indexers in C++.
/// </remarks>
public static ref TValue GetValueRefOrAddDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key)
{
return ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out _);
}
/// <summary>
/// Returns a <see langword="ref"/> to a value corresponding to a <typeparamref name="TKey"/> key in this <see cref="Dictionary{TKey, TValue}"/>.
/// Adds a <see langword="default"/> value if no existing value corresponds to the specified key.
/// </summary>
/// <remarks>
/// This has behavior similar to std::map indexers in C++.
/// </remarks>
public static ref TValue GetValueRefOrAddDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, out bool added)
{
ref TValue value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out bool exists);
added = !exists;
return ref value;
}
}
}

View file

@ -0,0 +1,24 @@
namespace MHServerEmu.Core.Extensions
{
public static class HashSetExtensions
{
public static void Set<T>(this HashSet<T> hashSet, HashSet<T> other)
{
hashSet.Clear();
foreach (T item in other)
hashSet.Add(item);
}
public static void Insert<T>(this HashSet<T> hashSet, HashSet<T> other)
{
foreach (T item in other)
hashSet.Add(item);
}
public static void Insert<T>(this HashSet<T> hashSet, T[] other)
{
foreach (T item in other)
hashSet.Add(item);
}
}
}

View file

@ -0,0 +1,37 @@
using System.Runtime.InteropServices;
namespace MHServerEmu.Core.Extensions
{
public static class ListExtensions
{
public static void Set<T>(this List<T> list, IEnumerable<T> other)
{
list.Clear();
list.AddRange(other);
}
/// <summary>
/// Adds the provided <see langword="struct"/> <typeparamref name="T"/> to this <see cref="List{T}"/> the specified number of times.
/// </summary>
public static void Fill<T>(this List<T> list, T value, int count) where T: struct
{
for (int i = 0; i < count; i++)
list.Add(value);
}
/// <summary>
/// Copies a range from another <see cref="List{T}"/> instance avoiding intermediary heap allocations.
/// </summary>
public static void AddRange<T>(this List<T> list, List<T> other, int start, int count)
{
Span<T> items = CollectionsMarshal.AsSpan(other);
items = items.Slice(start, count);
list.AddRange(items);
}
public static Span<T> AsSpan<T>(this List<T> list)
{
return CollectionsMarshal.AsSpan(list);
}
}
}

View file

@ -1,52 +0,0 @@
using System.Net;
namespace MHServerEmu.Core.Extensions
{
public static class MiscExtensions
{
/// <summary>
/// Returns a masked <see cref="string"/> representation of this <see cref="IPEndPoint"/>.
/// </summary>
/// <param name="endpoint"></param>
/// <returns></returns>
public static string ToStringMasked(this IPEndPoint endpoint)
{
string address = endpoint.Address.ToString();
return $"{address.Substring(0, address.Length / 2)}****:{endpoint.Port}";
}
public static void Set<T>(this List<T> list, IEnumerable<T> other)
{
list.Clear();
list.AddRange(other);
}
/// <summary>
/// Adds the provided <see langword="struct"/> <typeparamref name="T"/> to this <see cref="List{T}"/> the specified number of times.
/// </summary>
public static void Fill<T>(this List<T> list, T value, int count) where T: struct
{
for (int i = 0; i < count; i++)
list.Add(value);
}
public static void Set<T>(this HashSet<T> hashSet, HashSet<T> other)
{
hashSet.Clear();
foreach (T item in other)
hashSet.Add(item);
}
public static void Insert<T>(this HashSet<T> hashSet, HashSet<T> other)
{
foreach (T item in other)
hashSet.Add(item);
}
public static void Insert<T>(this HashSet<T> hashSet, T[] other)
{
foreach (T item in other)
hashSet.Add(item);
}
}
}

View file

@ -66,14 +66,13 @@ namespace MHServerEmu.Core.Extensions
if (count == 0)
return 0f;
List<float> list = ListPool<float>.Instance.Get(count);
using var listHandle = ListPool<float>.Instance.Get(count, out List<float> list);
foreach (float value in values)
list.Add(value);
list.Sort();
float median = list[count / 2];
ListPool<float>.Instance.Return(list);
return median;
}
}

View file

@ -0,0 +1,27 @@
using MHServerEmu.Core.Config;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Network.Web;
namespace MHServerEmu.Core.Extensions
{
public static class WebExtensions
{
private static readonly bool HideSensitiveInformation = ConfigManager.Instance.GetConfig<LoggingConfig>().HideSensitiveInformation;
// These are extensions rather thanWebRequestContext methods to keep the latter free
// from MHServerEmu specific dependencies.
public static string GetIPAddressHandle(this WebRequestContext context, out string ipAddress)
{
ipAddress = context.GetIPAddress();
// Hash the IP address to prevent it from appearing in logs if needed
return HideSensitiveInformation ? $"0x{HashHelper.Djb2(ipAddress):X8}" : ipAddress;
}
public static string GetIPAddressHandle(this WebRequestContext context)
{
return context.GetIPAddressHandle(out _);
}
}
}

View file

@ -57,9 +57,8 @@ namespace MHServerEmu.Core.Helpers
{
try
{
string json = File.ReadAllText(path);
T data = JsonSerializer.Deserialize<T>(json, options);
return data;
using FileStream fs = File.OpenRead(path);
return JsonSerializer.Deserialize<T>(fs, options);
}
catch (Exception e)
{
@ -73,11 +72,11 @@ namespace MHServerEmu.Core.Helpers
public static void SerializeJson<T>(string path, T @object, JsonSerializerOptions options = null)
{
string dirName = Path.GetDirectoryName(path);
if (Directory.Exists(dirName) == false)
if (string.IsNullOrWhiteSpace(dirName) == false && Directory.Exists(dirName) == false)
Directory.CreateDirectory(dirName);
string json = JsonSerializer.Serialize(@object, options);
File.WriteAllText(path, json);
using FileStream fs = File.Create(path);
JsonSerializer.Serialize(fs, @object, options);
}
/// <summary>

View file

@ -34,15 +34,23 @@ namespace MHServerEmu.Core.Helpers
return zlib.crc32(0, bytes, bytes.Length);
}
public static uint Djb2(ReadOnlySpan<byte> bytes)
{
uint hash = 5381;
foreach (byte b in bytes)
hash = (hash << 5) + hash + b;
return hash;
}
/// <summary>
/// Hashes a <see cref="string"/> using the djb2 algorithm.
/// </summary>
public static uint Djb2(string str)
{
uint hash = 5381;
for (int i = 0; i < str.Length; i++)
hash = (hash << 5) + hash + ((byte)str[i]);
return hash;
int numBytes = Encoding.UTF8.GetByteCount(str);
Span<byte> bytes = stackalloc byte[numBytes];
Encoding.UTF8.GetBytes(str, bytes);
return Djb2(bytes);
}
/// <summary>

View file

@ -1,131 +0,0 @@
using System.Text;
namespace MHServerEmu.Core.Helpers
{
/// <summary>
/// Interface for data structures that can be represented in HTML using <see cref="HtmlBuilder"/>.
/// </summary>
public interface IHtmlDataStructure
{
public void BuildHtml(StringBuilder sb);
}
/// <summary>
/// Helper functions to build HTML using <see cref="StringBuilder"/>.
/// </summary>
public static class HtmlBuilder
{
#region Headers
public static void AppendHeader1(StringBuilder sb, string text)
{
AppendDataLine(sb, "h1", text);
}
public static void AppendHeader2(StringBuilder sb, string text)
{
AppendDataLine(sb, "h2", text);
}
public static void AppendHeader3(StringBuilder sb, string text)
{
AppendDataLine(sb, "h3", text);
}
public static void AppendHeader4(StringBuilder sb, string text)
{
AppendDataLine(sb, "h4", text);
}
public static void AppendHeader5(StringBuilder sb, string text)
{
AppendDataLine(sb, "h5", text);
}
public static void AppendHeader6(StringBuilder sb, string text)
{
AppendDataLine(sb, "h6", text);
}
#endregion
#region Text
public static void AppendParagraph(StringBuilder sb, string text)
{
AppendDataLine(sb, "p", text);
}
#endregion
#region Lists
public static void BeginUnorderedList(StringBuilder sb)
{
sb.AppendLine("<ul>");
}
public static void EndUnorderedList(StringBuilder sb)
{
sb.AppendLine("</ul>");
}
public static void AppendListItem(StringBuilder sb, string item)
{
AppendDataLine(sb, "li", item);
}
#endregion
#region Tables
public static void BeginTable(StringBuilder sb)
{
sb.AppendLine("<table>");
}
public static void EndTable(StringBuilder sb)
{
sb.AppendLine("</table>");
}
public static void AppendTableRow(StringBuilder sb, params object[] data)
{
sb.Append("<tr>");
foreach (object dataIt in data)
AppendTableRowData(sb, dataIt);
sb.AppendLine("</tr>");
}
private static void AppendTableRowData(StringBuilder sb, object data)
{
AppendData(sb, "td", data);
}
#endregion
#region Custom Data Structures
public static void AppendDataStructure<T>(StringBuilder sb, in T htmlBuilder) where T: IHtmlDataStructure
{
htmlBuilder.BuildHtml(sb);
}
#endregion
#region Internal Common
private static void AppendData(StringBuilder sb, string tag, object data)
{
sb.AppendFormat("<{0}>{1}</{0}>", tag, data);
}
private static void AppendDataLine(StringBuilder sb, string tag, object data)
{
sb.AppendFormat("<{0}>{1}</{0}>", tag, data);
sb.AppendLine();
}
#endregion
}
}

View file

@ -1,6 +1,4 @@
using System.Text;
namespace MHServerEmu.Core.Logging
namespace MHServerEmu.Core.Logging
{
/// <summary>
/// A timestamped log message.
@ -9,8 +7,6 @@ namespace MHServerEmu.Core.Logging
{
private const string TimeFormat = "yyyy.MM.dd HH:mm:ss.fff";
private static readonly StringBuilder StringBuilder = new();
public DateTime Timestamp { get; }
public LoggingLevel Level { get; }
public string Logger { get; }
@ -36,26 +32,45 @@ namespace MHServerEmu.Core.Logging
public override string ToString()
{
return $"[{Timestamp.ToString(TimeFormat)}] [{Level,5}] [{Logger}] {Message}";
Span<char> formattedTimestamp = stackalloc char[TimeFormat.Length];
Timestamp.TryFormat(formattedTimestamp, out _, TimeFormat);
return $"[{formattedTimestamp}] [{Level,5}] [{Logger}] {Message}";
}
/// <summary>
/// Returns a string that represents this <see cref="LogMessage"/> with or without a timestamp.
/// </summary>
public string ToString(bool includeTimestamps)
public string ToString(bool includeTimestamp)
{
lock (StringBuilder) // This shouldn't be called from multiple threads unless in synchronous mode
if (includeTimestamp)
return ToString();
return $"[{Level,5}] [{Logger}] {Message}";
}
public void WriteTo(TextWriter writer, bool includeTimestamp, bool writeLine)
{
// TextWriter.Write() with format arguments causes less memory allocation than string interpolation as of .NET 8.
// We can't use this for our timestamp though because Span cannot be cast to an object.
// We can potentially make use of interpolated strings here without sacrificing performance with a custom InterpolatedStringHandler implementation.
// https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/interpolated-string-handler
if (includeTimestamp)
{
if (includeTimestamps)
StringBuilder.Append($"[{Timestamp.ToString(TimeFormat)}] ");
StringBuilder.Append($"[{Level,5}] [{Logger}] {Message}");
string str = StringBuilder.ToString();
StringBuilder.Clear();
return str;
Span<char> formattedTimestamp = stackalloc char[TimeFormat.Length];
Timestamp.TryFormat(formattedTimestamp, out _, TimeFormat);
writer.Write('[');
writer.Write(formattedTimestamp);
writer.Write(']');
writer.Write(' ');
}
// Enum.GetName() doesn't allocate memory unlike ToString().
writer.Write("[{0,5}] [{1}] {2}", Enum.GetName(Level), Logger, Message);
if (writeLine)
writer.WriteLine();
}
}
}

View file

@ -1,5 +1,6 @@
using MHServerEmu.Core.Config;
using System.Collections.Concurrent;
using System.Globalization;
namespace MHServerEmu.Core.Logging
{
@ -8,7 +9,8 @@ namespace MHServerEmu.Core.Logging
/// </summary>
internal static class LogRouter
{
private static readonly ConcurrentQueue<LogMessage> MessageQueue;
private static readonly BlockingCollection<LogMessage> LogMessages;
private static readonly Thread LogThread;
/// <summary>
/// Initializes <see cref="LogRouter"/>.
@ -17,51 +19,53 @@ namespace MHServerEmu.Core.Logging
{
// Initialize async logging if synchronous mode is not enabled
var config = ConfigManager.Instance.GetConfig<LoggingConfig>();
if (config.SynchronousMode == false)
if (config.SynchronousMode)
return;
LogMessages = new();
LogThread = new(RouteLogMessages)
{
MessageQueue = new();
Task.Run(async () => await RouteMessagesAsync());
}
Name = "Logging",
IsBackground = true,
CurrentCulture = CultureInfo.InvariantCulture
};
LogThread.Start();
}
/// <summary>
/// Creates a new <see cref="LogMessage"/> instance from the provided arguments and processes it.
/// Add a <see cref="LogMessage"/> instance to be routed.
/// </summary>
internal static void AddMessage(LoggingLevel level, string logger, string message, LogChannels channels, LogCategory category)
internal static void AddLogMessage(in LogMessage logMessage)
{
if (LogManager.Enabled == false) return;
if (LogManager.Enabled == false)
return;
LogMessage logMessage = new(level, logger, message, channels, category);
if (MessageQueue != null)
MessageQueue.Enqueue(logMessage); // Add the message to the queue to be processed asynchronously
if (LogMessages != null)
LogMessages.Add(logMessage); // Add the message to the queue to be routed asynchronously
else
RouteMessage(logMessage); // Process the message right away if async output is disabled (note: this is slow and should be used only for testing)
RouteLogMessage(logMessage); // Route the message right away if async output is disabled (this is slow and should be used only for testing)
}
/// <summary>
/// Routes the provided <see cref="LogMessage"/> instance to all relevant targets.
/// </summary>
private static void RouteMessage(in LogMessage message)
private static void RouteLogMessage(in LogMessage logMessage)
{
foreach (LogTarget target in LogManager.IterateTargets(message))
target.ProcessLogMessage(message);
foreach (LogTarget target in LogManager.IterateTargets(logMessage))
target.ProcessLogMessage(logMessage);
}
/// <summary>
/// Processes enqueued <see cref="LogMessage"/> instances asynchronously.
/// Processes enqueued <see cref="LogMessage"/>. This should run on its own thread.
/// </summary>
private static async Task RouteMessagesAsync()
private static void RouteLogMessages()
{
while (true)
{
while (MessageQueue.IsEmpty == false)
{
if (MessageQueue.TryDequeue(out LogMessage message))
RouteMessage(message);
}
await Task.Delay(1);
LogMessage logMessage = LogMessages.Take();
RouteLogMessage(logMessage);
}
}
}

View file

@ -173,7 +173,8 @@ namespace MHServerEmu.Core.Logging
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Log(LoggingLevel level, string message, LogChannels channels = LogChannels.General, LogCategory category = LogCategory.Common)
{
LogRouter.AddMessage(level, _name, message, channels, category);
LogMessage logMessage = new(level, _name, message, channels, category);
LogRouter.AddLogMessage(logMessage);
}
/// <summary>

View file

@ -18,7 +18,7 @@
public override void ProcessLogMessage(in LogMessage message)
{
SetForegroundColor(message.Level);
Console.WriteLine(message.ToString(IncludeTimestamps));
message.WriteTo(Console.Out, IncludeTimestamps, true);
Console.ResetColor();
}

View file

@ -31,7 +31,7 @@ namespace MHServerEmu.Core.Logging.Targets
{
string filePath = Path.Combine(logDirectory, $"{fileName}_{category}.log");
FileStream fs = new(filePath, fileMode, FileAccess.Write, FileShare.Read);
_writers[(int)category] = new(fs) { AutoFlush = true };
_writers[(int)category] = new(fs);
}
}
else
@ -39,7 +39,7 @@ namespace MHServerEmu.Core.Logging.Targets
// Create a single writer for all categories
string filePath = Path.Combine(logDirectory, $"{fileName}.log");
FileStream fs = new(filePath, fileMode, FileAccess.Write, FileShare.Read);
_writers = [new(fs) { AutoFlush = true }];
_writers = [new(fs)];
}
}
@ -51,8 +51,9 @@ namespace MHServerEmu.Core.Logging.Targets
if (_disposed)
return;
int index = _splitOutput ? (int)message.Category : 0;
_writers[index].WriteLine(message.ToString(IncludeTimestamps));
StreamWriter writer = _writers[_splitOutput ? (int)message.Category : 0];
message.WriteTo(writer, IncludeTimestamps, true);
writer.Flush();
}
#region IDisposable Implementation

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
</PropertyGroup>

View file

@ -1,4 +1,5 @@
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Collections;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Memory
{
@ -9,7 +10,7 @@ namespace MHServerEmu.Core.Memory
{
// NOTE: We use a separate class to have shared settings for various CollectionPool types.
// For game threads we want to have dedicated pools, in other cases we'll use shared pools with locks
// For game threads we want to have dedicated pools, in other cases we use shared pools with locks
[ThreadStatic]
public static bool UseThreadLocalStorage;
}
@ -43,6 +44,16 @@ namespace MHServerEmu.Core.Memory
}
}
/// <summary>
/// Retrieves a <typeparamref name="TCollection"/> from the pool or allocates a new one if the pool is empty.
/// Returns a <see cref="CollectionHandle"/> that can automatically return the <typeparamref name="TCollection"/> instance to the pool when it goes out of scope.
/// </summary>
public CollectionHandle Get(out TCollection collection)
{
collection = Get();
return new(this, collection);
}
/// <summary>
/// Clears the provided <typeparamref name="TCollection"/> and returns it to the pool.
/// </summary>
@ -61,7 +72,27 @@ namespace MHServerEmu.Core.Memory
}
/// <summary>
/// Represents a storage unit of a pool or a particular type.
/// A handle that implements <see cref="IDisposable"/> that can automatically return a <typeparamref name="TCollection"/> instance to the pool when it goes out of scope.
/// </summary>
public readonly struct CollectionHandle : IDisposable
{
private readonly CollectionPool<TCollection, TValue> _pool;
private readonly TCollection _collection;
public CollectionHandle(CollectionPool<TCollection, TValue> pool, TCollection collection)
{
_pool = pool;
_collection = collection;
}
public void Dispose()
{
_pool.Return(_collection);
}
}
/// <summary>
/// Represents a storage unit of a pool of a particular type.
/// </summary>
private class Node
{
@ -111,7 +142,7 @@ namespace MHServerEmu.Core.Memory
private ListPool() { }
/// <summary>
/// Retrieves a <typeparamref name="TCollection"/> from the pool or allocates a new one if the pool is empty and ensures it has the specified capacity.
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and ensures it has the specified capacity.
/// </summary>
public List<T> Get(int capacity)
{
@ -121,7 +152,19 @@ namespace MHServerEmu.Core.Memory
}
/// <summary>
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and copies all elements from collection.
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and ensures it has the specified capacity.
/// Returns a <see cref="CollectionPool{TCollection, TValue}.CollectionHandle"/> that can automatically return the <see cref="List{T}"/>
/// instance to the pool when it goes out of scope.
/// </summary>
public CollectionHandle Get(int capacity, out List<T> list)
{
CollectionHandle handle = Get(out list);
list.EnsureCapacity(capacity);
return handle;
}
/// <summary>
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and copies all elements from the provided <see cref="IEnumerable{T}"/> collection.
/// </summary>
public List<T> Get(IEnumerable<T> collection)
{
@ -129,6 +172,17 @@ namespace MHServerEmu.Core.Memory
list.AddRange(collection);
return list;
}
/// <summary>
/// Retrieves a <see cref="List{T}"/> from the pool or allocates a new one if the pool is empty and copies all elements from the provided <see cref="IEnumerable{T}"/> collection.
/// Returns a <see cref="CollectionPool{TCollection, TValue}.CollectionHandle"/> that can automatically return the <see cref="List{T}"/> instance to the pool when it goes out of scope.
/// </summary>
public CollectionHandle Get(IEnumerable<T> collection, out List<T> list)
{
CollectionHandle handle = Get(out list);
list.AddRange(collection);
return handle;
}
}
/// <summary>
@ -150,4 +204,14 @@ namespace MHServerEmu.Core.Memory
private HashSetPool() { }
}
/// <summary>
/// Provides a pool of reusable <see cref="PoolableStack{T}"/> instances, similar to ArrayPool.
/// </summary>
public sealed class StackPool<T> : CollectionPool<PoolableStack<T>, T>
{
public static StackPool<T> Instance { get; } = new();
private StackPool() { }
}
}

View file

@ -0,0 +1,37 @@
using System.Collections.Concurrent;
using System.Diagnostics;
namespace MHServerEmu.Core.Memory
{
/// <summary>
/// A thread-safe pool of arbitrary objects intended to be used for cases where retrieval and returns always happen on separate threads.
/// </summary>
public class ConcurrentPool<T>
{
private readonly ConcurrentStack<T> _items = new();
private readonly int _maxCount;
private readonly Func<T> _constructor;
public ConcurrentPool(int maxCount, Func<T> constructor)
{
Debug.Assert(maxCount > 0);
_maxCount = maxCount;
_constructor = constructor;
}
public T Get()
{
if (_items.TryPop(out T item) == false)
return _constructor();
return item;
}
public void Return(T item)
{
if (_items.Count < _maxCount)
_items.Push(item);
}
}
}

View file

@ -0,0 +1,56 @@
using System.Buffers;
namespace MHServerEmu.Core.Memory
{
/// <summary>
/// A wrapper around <see cref="Span{T}"/> for automatically slicing and disposing memory rented from <see cref="ArrayPool{T}"/>.
/// </summary>
/// <remarks>
/// This is similar to SpanOwner from the .NET Community Toolkit.
/// </remarks>
public readonly ref struct PoolSpan<T>
{
private readonly ArrayPool<T> _pool;
private readonly T[] _buffer;
private readonly bool _clearOnDispose;
public Span<T> Span { get; }
public int Length { get => Span.Length; }
public T this[int index] { get => Span[index]; set => Span[index] = value; }
private PoolSpan(ArrayPool<T> pool, T[] buffer, int length, bool clearOnDispose)
{
_pool = pool;
_buffer = buffer;
Span = buffer.AsSpan(0, length);
_clearOnDispose = clearOnDispose;
}
public static PoolSpan<T> Allocate(int length, bool clearOnDispose, ArrayPool<T> pool)
{
T[] buffer = pool.Rent(length);
return new(pool, buffer, length, clearOnDispose);
}
public static PoolSpan<T> Allocate(int length, bool clearOnDispose = true)
{
return Allocate(length, clearOnDispose, ArrayPool<T>.Shared);
}
public Span<T>.Enumerator GetEnumerator()
{
return Span.GetEnumerator();
}
public void Clear()
{
Span.Clear();
}
public void Dispose()
{
_pool.Return(_buffer, _clearOnDispose);
}
}
}

View file

@ -1,5 +1,4 @@
using System.Text;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Metrics.Categories
@ -79,7 +78,7 @@ namespace MHServerEmu.Core.Metrics.Categories
return _trackers[(int)metric].AsReportEntry();
}
public readonly struct Report : IHtmlDataStructure
public readonly struct Report
{
public MetricTracker.ReportEntry UpdateTime { get; }
public MetricTracker.ReportEntry FrameTime { get; }
@ -106,21 +105,6 @@ namespace MHServerEmu.Core.Metrics.Categories
sb.AppendLine($"{nameof(PlayerCount)}: {PlayerCount}");
return sb.ToString();
}
public void BuildHtml(StringBuilder sb)
{
HtmlBuilder.BeginTable(sb);
HtmlBuilder.AppendTableRow(sb, "Metric", "Avg", "Mdn", "Last", "Min", "Max");
HtmlBuilder.AppendDataStructure(sb, UpdateTime);
HtmlBuilder.AppendDataStructure(sb, FrameTime);
HtmlBuilder.AppendDataStructure(sb, ScheduledEventsPerUpdate);
HtmlBuilder.AppendDataStructure(sb, EntityCount);
HtmlBuilder.AppendDataStructure(sb, PlayerCount);
HtmlBuilder.EndTable(sb);
}
}
}
}

View file

@ -1,6 +1,5 @@
using System.Runtime;
using System.Text;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Metrics.Categories
@ -63,7 +62,7 @@ namespace MHServerEmu.Core.Metrics.Categories
}
}
public readonly struct Report : IHtmlDataStructure
public readonly struct Report
{
public long GCIndex { get; }
public long GCCountGen0 { get; }
@ -98,19 +97,6 @@ namespace MHServerEmu.Core.Metrics.Categories
return sb.ToString();
}
public void BuildHtml(StringBuilder sb)
{
HtmlBuilder.BeginUnorderedList(sb);
HtmlBuilder.AppendListItem(sb, $"{nameof(GCIndex)}: {GCIndex}");
HtmlBuilder.AppendListItem(sb, $"GCCounts: Gen0={GCCountGen0}, Gen1={GCCountGen1}, Gen2={GCCountGen2}");
HtmlBuilder.AppendListItem(sb, $"{nameof(HeapSizeBytes)}: {HeapSizeBytes:N0} / {TotalCommittedBytes:N0}");
HtmlBuilder.AppendListItem(sb, $"{nameof(PauseTimePercentage)}: {PauseTimePercentage}%");
HtmlBuilder.AppendListItem(sb, $"{nameof(PauseDuration)}: {PauseDuration}");
HtmlBuilder.EndUnorderedList(sb);
}
}
}
}

View file

@ -1,7 +1,5 @@
using System.Text;
using MHServerEmu.Core.Collections;
using MHServerEmu.Core.Collections;
using MHServerEmu.Core.Extensions;
using MHServerEmu.Core.Helpers;
namespace MHServerEmu.Core.Metrics
{
@ -60,9 +58,8 @@ namespace MHServerEmu.Core.Metrics
/// <summary>
/// A snapshot of the state of a <see cref="MetricTracker"/>.
/// </summary>
public readonly struct ReportEntry : IHtmlDataStructure
public readonly struct ReportEntry
{
public string Name { get; }
public float Average { get; }
public float Median { get; }
public float Last { get; }
@ -71,7 +68,6 @@ namespace MHServerEmu.Core.Metrics
public ReportEntry(MetricTracker tracker)
{
Name = tracker._name;
Average = tracker._buffer.ToAverage();
Median = tracker._buffer.ToMedian();
Last = tracker._last;
@ -83,17 +79,6 @@ namespace MHServerEmu.Core.Metrics
{
return $"avg={Average}, mdn={Median}, last={Last}, min={Min}, max={Max}";
}
public void BuildHtml(StringBuilder sb)
{
HtmlBuilder.AppendTableRow(sb,
Name,
Average.ToString("0.00"),
Median.ToString("0.00"),
Last.ToString("0.00"),
Min != float.MaxValue ? Min.ToString("0.00") : "0.00",
Max != float.MinValue ? Max.ToString("0.00") : "0.00");
}
}
}
}

View file

@ -4,7 +4,6 @@
{
PlainText,
Json,
Html,
}
public enum GamePerformanceMetricEnum

View file

@ -76,12 +76,17 @@ namespace MHServerEmu.Core.Metrics
_gameInstancesToRemove.Enqueue(gameId);
}
public void GetPerformanceReportData(PerformanceReport report)
{
lock (_lock)
report.Initialize(_memoryMetrics, _gamePerformanceMetricsDict);
}
public string GeneratePerformanceReport(MetricsReportFormat format)
{
using PerformanceReport report = ObjectPoolManager.Instance.Get<PerformanceReport>();
lock (_lock)
report.Initialize(_memoryMetrics, _gamePerformanceMetricsDict.Values);
GetPerformanceReportData(report);
return report.ToString(format);
}

View file

@ -1,6 +1,6 @@
using System.Text;
using System.Text.Json;
using MHServerEmu.Core.Helpers;
using System.Text.Json.Serialization;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Memory;
using MHServerEmu.Core.Metrics.Categories;
@ -14,21 +14,23 @@ namespace MHServerEmu.Core.Metrics
private static uint _currentReportId = 0;
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
public ulong Id { get; private set; }
public MemoryMetrics.Report Memory { get; private set; }
public Dictionary<ulong, GamePerformanceMetrics.Report> Games { get; } = new();
[JsonIgnore]
public bool IsInPool { get; set; }
public PerformanceReport() { }
public void Initialize(MemoryMetrics memoryMetrics, IEnumerable<GamePerformanceMetrics> gameMetrics)
public void Initialize(MemoryMetrics memoryMetrics, Dictionary<ulong, GamePerformanceMetrics> gameMetrics)
{
Id = (ulong)Clock.UnixTime.TotalSeconds << 32 | ++_currentReportId;
Memory = memoryMetrics.GetReport();
foreach (GamePerformanceMetrics metrics in gameMetrics)
foreach (GamePerformanceMetrics metrics in gameMetrics.Values)
{
Games.Add(metrics.GameId, metrics.GetReport());
}
@ -49,9 +51,6 @@ namespace MHServerEmu.Core.Metrics
case MetricsReportFormat.Json:
return JsonSerializer.Serialize(this);
case MetricsReportFormat.Html:
return AsHtml();
default:
return Logger.WarnReturn(string.Empty, $"ToString(): Unsupported format {format}");
}
@ -85,24 +84,5 @@ namespace MHServerEmu.Core.Metrics
return sb.ToString();
}
private string AsHtml()
{
StringBuilder sb = new();
HtmlBuilder.AppendParagraph(sb, $"Report 0x{Id:X}");
HtmlBuilder.AppendHeader2(sb, "Memory");
HtmlBuilder.AppendDataStructure(sb, Memory);
HtmlBuilder.AppendHeader2(sb, "Games");
foreach (var kvp in Games.OrderBy(kvp => kvp.Key))
{
HtmlBuilder.AppendHeader3(sb, $"0x{kvp.Key:X}");
HtmlBuilder.AppendDataStructure(sb, kvp.Value);
}
return sb.ToString();
}
}
}

View file

@ -29,6 +29,22 @@ namespace MHServerEmu.Core.Network
RemoveResponse,
}
public enum ChatRoomOperationType
{
Add,
Remove,
}
public enum AccountOperation
{
Create,
SetPlayerName,
SetPassword,
SetUserLevel,
SetFlag,
ClearFlag,
}
#endregion
public static class ServiceMessage
@ -65,7 +81,7 @@ namespace MHServerEmu.Core.Network
public readonly MailboxMessage Message = message;
}
#region Game Instances
#region Player Manager
public readonly struct GameInstanceOp(GameInstanceOpType type, ulong gameId)
: IGameServiceMessage
@ -104,6 +120,16 @@ namespace MHServerEmu.Core.Network
public readonly bool Success = success;
}
/// <summary>
/// [Game -> PlayerManager] Requests <see cref="RegionPlayerAccessVar"/> update for a region.
/// </summary>
public readonly struct SetRegionPlayerAccess(ulong regionId, RegionPlayerAccessVar playerAccess)
: IGameServiceMessage
{
public readonly ulong RegionId = regionId;
public readonly RegionPlayerAccessVar PlayerAccess = playerAccess;
}
/// <summary>
/// [Game -> PlayerManager] Requests the player manager to shut down the specified region.
/// </summary>
@ -230,6 +256,15 @@ namespace MHServerEmu.Core.Network
public readonly ulong DifficultyTierProtoId = difficultyTierProtoId;
}
/// <summary>
/// [Game -> PlayerManager] Notifies the Player Manager that a save happened and player data needs to be written to the database.
/// </summary>
public readonly struct PlayerDataUpdated(ulong playerDbId)
: IGameServiceMessage
{
public readonly ulong PlayerDbId = playerDbId;
}
/// <summary>
/// [Game -> PlayerManager] Requests player dbid and properly cased name from the player manager.
/// </summary>
@ -328,6 +363,9 @@ namespace MHServerEmu.Core.Network
public readonly PartyOperationPayload Request = request;
}
/// <summary>
/// [Game -> PlayerManager] Notifies the Player Manager of a player's current party boosts.
/// </summary>
public readonly struct PartyBoostUpdate(ulong playerDbId, List<ulong> boosts)
: IGameServiceMessage
{
@ -375,33 +413,159 @@ namespace MHServerEmu.Core.Network
public readonly PartyMemberInfo MemberInfo = memberInfo;
}
/// <summary>
/// [PlayerManager -> Game] Notifies a player of a party kick grace period before they are removed from the current region.
/// </summary>
public readonly struct PartyKickGracePeriod(ulong gameId, ulong playerDbId, ulong expireTimeMicroseconds, GroupLeaveReason leaveReason)
: IGameServiceMessage
{
public readonly ulong GameId = gameId;
public readonly ulong PlayerDbId = playerDbId;
public readonly ulong ExpireTimeMicroseconds = expireTimeMicroseconds;
public readonly GroupLeaveReason LeaveReason = leaveReason;
}
/// <summary>
/// [Game -> PlayerManager] Routes guild messages to the player manager.
/// </summary>
public readonly struct GuildMessageToPlayerManager(GuildMessageSetToPlayerManager messages)
: IGameServiceMessage
{
public readonly GuildMessageSetToPlayerManager Messages = messages;
}
// NOTE: In the protocol for 1.53 there is a single GuildMessageToServer protobuf that is used for both client and server guild messages.
// Server messages don't actually need a list of players, and it makes more sense to split them.
/// <summary>
/// [PlayerManager -> Game] Routes guild messages from the player manager to a game instance.
/// </summary>
public readonly struct GuildMessageToServer(ulong gameId, GuildMessageSetToServer serverMessages)
: IGameServiceMessage
{
public readonly ulong GameId = gameId;
public readonly GuildMessageSetToServer Messages = serverMessages;
}
/// <summary>
/// [PlayerManager -> Game] Routes guilds messages from the player manager to a client in a game instance.
/// </summary>
public readonly struct GuildMessageToClient(ulong gameId, ulong playerDbId, GuildMessageSetToClient messages)
: IGameServiceMessage
{
public readonly ulong GameId = gameId;
public readonly ulong PlayerDbId = playerDbId;
public readonly GuildMessageSetToClient Messages = messages;
}
/// <summary>
/// [Game -> PlayerManager] Relays a match region request command from a client.
/// </summary>
public readonly struct MatchRegionRequestQueueCommand(ulong playerDbId, ulong regionProtoId, ulong difficultyTierProtoId, ulong metaStateProtoId, RegionRequestQueueCommandVar command, ulong regionRequestGroupId, ulong targetPlayerDbId, int teamSizeOverride)
: IGameServiceMessage
{
public readonly ulong PlayerDbId = playerDbId;
public readonly ulong RegionProtoId = regionProtoId;
public readonly ulong DifficultyTierProtoId = difficultyTierProtoId;
public readonly ulong MetaStateProtoId = metaStateProtoId;
public readonly RegionRequestQueueCommandVar Command = command;
public readonly ulong RegionRequestGroupId = regionRequestGroupId;
public readonly ulong TargetPlayerDbId = targetPlayerDbId;
public readonly int TeamSizeOverride = teamSizeOverride;
}
// MatchQueueUpdate is based on PlayerMgrToGameServer.proto from 1.53
public readonly struct MatchQueueUpdateData(ulong updatePlayerGuid, RegionRequestQueueUpdateVar status, string updatePlayerName = null)
{
public readonly ulong UpdatePlayerGuid = updatePlayerGuid;
public readonly RegionRequestQueueUpdateVar Status = status;
public readonly string UpdatePlayerName = updatePlayerName;
}
/// <summary>
/// [PlayerManager -> Game] Updates the state of a MatchQueueStatus instance game-side.
/// </summary>
public readonly struct MatchQueueUpdate(ulong gameId, ulong playerDbId, ulong regionProtoId, ulong difficultyTierProtoId, int playersInQueue, ulong regionRequestGroupId, List<MatchQueueUpdateData> data)
: IGameServiceMessage
{
public readonly ulong GameId = gameId;
public readonly ulong PlayerDbId = playerDbId;
public readonly ulong RegionProtoId = regionProtoId;
public readonly ulong DifficultyTierProtoId = difficultyTierProtoId;
public readonly int PlayersInQueue = playersInQueue;
public readonly ulong RegionRequestGroupId = regionRequestGroupId;
public readonly List<MatchQueueUpdateData> Data = data;
}
/// <summary>
/// [PlayerManager -> Game] Clears the state of a MatchQueueStatus instance game-side.
/// </summary>
public readonly struct MatchQueueFlush(ulong gameId, ulong playerDbId)
: IGameServiceMessage
{
public readonly ulong GameId = gameId;
public readonly ulong PlayerDbId = playerDbId;
}
#endregion
#region Grouping Manager
public readonly struct GroupingManagerChat(IFrontendClient client, NetMessageChat chat, int prestigeLevel, List<ulong> playerFilter)
/// <summary>
/// [Game -> GroupingManager] Routes a regular chat message from a game instance.
/// </summary>
public readonly struct GroupingManagerChat(ulong playerDbId, NetMessageChat chat, int prestigeLevel, List<ulong> playerFilter)
: IGameServiceMessage
{
public readonly IFrontendClient Client = client;
public readonly ulong PlayerDbId = playerDbId;
public readonly NetMessageChat Chat = chat;
public readonly int PrestigeLevel = prestigeLevel;
public readonly List<ulong> PlayerFilter = playerFilter;
}
public readonly struct GroupingManagerTell(IFrontendClient client, NetMessageTell tell, int prestigeLevel)
/// <summary>
/// [Game -> GroupingManager] Routes a tell chat message from a game instance.
/// </summary>
public readonly struct GroupingManagerTell(ulong playerDbId, NetMessageTell tell, int prestigeLevel)
: IGameServiceMessage
{
public readonly IFrontendClient Client = client;
public readonly ulong PlayerDbId = playerDbId;
public readonly NetMessageTell Tell = tell;
public readonly int PrestigeLevel = prestigeLevel;
}
/// <summary>
/// [Any -> GroupingManager] Sends a custom metagame chat message to the specified player.
/// </summary>
public readonly struct GroupingManagerMetagameMessage(ulong playerDbId, string text, bool showSender)
: IGameServiceMessage
{
public readonly ulong PlayerDbId = playerDbId;
public readonly string Text = text;
public readonly bool ShowSender = showSender;
}
/// <summary>
/// [Command -> GroupingManager] Broadcasts a server notification to all connected clients.
/// </summary>
public readonly struct GroupingManagerServerNotification(string notificationText)
: IGameServiceMessage
{
public readonly string NotificationText = notificationText;
}
/// <summary>
/// [PlayerManager -> GroupingManager] Adds/removes a player to/from the specified chat room.
/// </summary>
public readonly struct GroupingManagerChatRoomOperation(ChatRoomTypes roomType, ulong roomId, ulong playerDbId, ChatRoomOperationType operation)
: IGameServiceMessage
{
public readonly ChatRoomTypes RoomType = roomType;
public readonly ulong RoomId = roomId;
public readonly ulong PlayerDbId = playerDbId;
public readonly ChatRoomOperationType Operation = operation;
}
#endregion
#region Leaderboards
@ -530,5 +694,149 @@ namespace MHServerEmu.Core.Network
}
#endregion
#region Auth
// WebFrontend -> PlayerManager
public readonly struct AuthRequest(ulong requestId, LoginDataPB loginDataPB)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly LoginDataPB LoginDataPB = loginDataPB;
}
// PlayerManager -> WebFrontend
public readonly struct AuthResponse(ulong requestId, int statusCode, AuthTicket authTicket)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly int StatusCode = statusCode;
public readonly AuthTicket AuthTicket = authTicket;
}
// Frontend -> PlayerManager
public readonly struct SessionVerificationRequest(IFrontendClient client, ClientCredentials clientCredentials)
: IGameServiceMessage
{
public readonly IFrontendClient Client = client;
public readonly ClientCredentials ClientCredentials = clientCredentials;
}
#endregion
#region MTXStore
// WebFrontend -> PlayerManager
public readonly struct MTXStoreESBalanceRequest(ulong requestId, string email, string token)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly string Email = email;
public readonly string Token = token;
}
// PlayerManager -> WebFrontend
public readonly struct MTXStoreESBalanceResponse(ulong requestId, int statusCode, int currentBalance = 0, float conversionRatio = 0, int conversionStep = 0)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly int StatusCode = statusCode;
public readonly int CurrentBalance = currentBalance;
public readonly float ConversionRatio = conversionRatio;
public readonly int ConversionStep = conversionStep;
}
// PlayerManager -> Game
public readonly struct MTXStoreESBalanceGameRequest(ulong requestId, ulong gameId, ulong playerDbId)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly ulong GameId = gameId;
public readonly ulong PlayerDbId = playerDbId;
}
// Game -> PlayerManager
public readonly struct MTXStoreESBalanceGameResponse(ulong requestId, int currentBalance, float conversionRatio, int conversionStep)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly int CurrentBalance = currentBalance;
public readonly float ConversionRatio = conversionRatio;
public readonly int ConversionStep = conversionStep;
}
// WebFrontend -> PlayerManager
public readonly struct MTXStoreESConvertRequest(ulong requestId, string email, string token, int amount)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly string Email = email;
public readonly string Token = token;
public readonly int Amount = amount;
}
// PlayerManager -> WebFrontend
public readonly struct MTXStoreESConvertResponse(ulong requestId, int statusCode)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly int StatusCode = statusCode;
}
// PlayerManager -> Game
public readonly struct MTXStoreESConvertGameRequest(ulong requestId, ulong gameId, ulong playerDbId, int amount)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly ulong GameId = gameId;
public readonly ulong PlayerDbId = playerDbId;
public readonly int Amount = amount;
}
// Game -> PlayerManager
public readonly struct MTXStoreESConvertGameResponse(ulong requestId, bool result)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly bool Result = result;
}
#endregion
#region Account
/// <summary>
/// [WebFrontend -> PlayerManager] Routes an account operation request to the Player Manager service.
/// </summary>
public readonly struct AccountOperationRequest(ulong requestId, AccountOperation operation, string email,
string playerName, string password, byte userLevel, int flags)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly AccountOperation Operation = operation;
public readonly string Email = email;
public readonly string PlayerName = playerName;
public readonly string Password = password;
public readonly byte UserLevel = userLevel;
public readonly int Flags = flags;
}
/// <summary>
/// [PlayerManager -> WebFrontend] Routes a response to an account operation request back to the Web Frontend.
/// </summary>
public readonly struct AccountOperationResponse(ulong requestId, int resultCode)
: IGameServiceMessage
{
public readonly ulong RequestId = requestId;
public readonly int ResultCode = resultCode;
}
public readonly struct SetWhitelistEnabled(bool enable)
: IGameServiceMessage
{
public readonly bool Enable = enable;
}
#endregion
}
}

View file

@ -7,5 +7,6 @@
{
public ulong Id { get; }
public object Account { get; } // Not having this be strongly typed is not ideal, but it allows us to avoid coupling Core and DatabaseAccess.
public string Locale { get; }
}
}

View file

@ -23,8 +23,8 @@
public void ReceiveServiceMessage<T>(in T message) where T: struct, IGameServiceMessage;
/// <summary>
/// Returns a <see cref="string"/> representing the status of this <see cref="IGameService"/>.
/// Adds the status of this <see cref="IGameService"/> to the provided dictionary.
/// </summary>
public string GetStatus();
public void GetStatus(Dictionary<string, long> statusDict);
}
}

View file

@ -1,4 +1,5 @@
using Google.ProtocolBuffers;
using MHServerEmu.Core.Serialization;
namespace MHServerEmu.Core.Network
{
@ -43,5 +44,15 @@ namespace MHServerEmu.Core.Network
stream.WriteRawVarint32((uint)Message.SerializedSize);
Message.WriteTo(stream);
}
/// <summary>
/// Writes this <see cref="MessagePackageOut"/> to the provided <see cref="ICodedOutputStreamEx"/>.
/// </summary>
public void WriteTo(ICodedOutputStreamEx stream)
{
stream.WriteRawVarint32(Id);
stream.WriteRawVarint32((uint)Message.SerializedSize);
Message.WriteTo(stream);
}
}
}

View file

@ -1,9 +1,9 @@
using System.Buffers;
using System.Collections;
using System.Collections;
using Google.ProtocolBuffers;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Memory;
using MHServerEmu.Core.Network.Tcp;
using MHServerEmu.Core.Serialization;
namespace MHServerEmu.Core.Network
{
@ -13,7 +13,9 @@ namespace MHServerEmu.Core.Network
public readonly struct MuxPacket : IPacket
{
private static readonly Logger Logger = LogManager.CreateLogger();
private static readonly ArrayPool<byte> BufferPool = ArrayPool<byte>.Create();
// Packets apparently go as high as 2800+ messages based on logs, so we presize pooled lists to 4096 to fit that and extra.
private static readonly ConcurrentPool<List<MessagePackageOut>> MessageListPool = new(4096, static () => new(4096));
private readonly List<MessagePackageOut> _outboundMessageList = null;
@ -39,7 +41,16 @@ namespace MHServerEmu.Core.Network
Command = command;
if (IsDataPacket)
_outboundMessageList = new();
_outboundMessageList = MessageListPool.Get();
}
public void Dispose()
{
if (IsDataPacket)
{
_outboundMessageList.Clear();
MessageListPool.Return(_outboundMessageList);
}
}
/// <summary>
@ -63,8 +74,6 @@ namespace MHServerEmu.Core.Network
if (IsDataPacket == false)
return Logger.WarnReturn(false, "AddMessages(): Attempted to add messages to a non-data packet");
_outboundMessageList.EnsureCapacity(_outboundMessageList.Count + messageList.Count);
foreach (IMessage message in messageList)
{
MessagePackageOut messagePackage = new(message);
@ -126,15 +135,10 @@ namespace MHServerEmu.Core.Network
if (_outboundMessageList.Count == 0)
return Logger.WarnReturn(false, "SerializeData(): Data packet contains no messages");
// Use pooled buffers for coded output streams with reflection hackery, see ProtobufHelper for more info.
byte[] buffer = BufferPool.Rent(4096);
using RecyclableCodedOutputStream cos = RecyclableCodedOutputStream.CreateInstance(stream);
CodedOutputStream cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, buffer);
foreach (MessagePackageOut messagePackage in _outboundMessageList)
messagePackage.WriteTo(cos);
cos.Flush();
BufferPool.Return(buffer);
return true;
}

View file

@ -1,7 +1,7 @@
using System.Globalization;
using System.Text;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Metrics;
using MHServerEmu.Core.Memory;
using MHServerEmu.Core.System.Time;
namespace MHServerEmu.Core.Network
@ -13,9 +13,8 @@ namespace MHServerEmu.Core.Network
Leaderboard,
PlayerManager,
GroupingManager,
Billing,
Frontend,
Auth,
WebFrontend,
NumServiceTypes
}
@ -234,33 +233,30 @@ namespace MHServerEmu.Core.Network
_state = ServerManagerState.Shutdown;
}
/// <summary>
/// Adds structured server status data to the provided dictionary.
/// </summary>
public void GetServerStatus(Dictionary<string, long> statusDict)
{
statusDict["StartupTime"] = (long)StartupTime.TotalSeconds;
statusDict["CurrentTime"] = (long)Clock.UnixTime.TotalSeconds;
for (int i = 0; i < _services.Length; i++)
_services[i]?.GetStatus(statusDict);
}
/// <summary>
/// Returns a <see cref="string"/> representing the current status of all running <see cref="IGameService"/> instances.
/// </summary>
public string GetServerStatus(bool includeMetrics)
public string GetServerStatusString()
{
using var statusDictHandle = DictionaryPool<string, long>.Instance.Get(out Dictionary<string, long> statusDict);
GetServerStatus(statusDict);
StringBuilder sb = new();
TimeSpan uptime = Clock.UnixTime - StartupTime;
sb.AppendLine($"Uptime: {uptime:dd\\:hh\\:mm\\:ss}");
sb.AppendLine("Service Status:");
for (int i = 0; i < _services.Length; i++)
{
if (_services[i] == null) continue;
sb.Append($"[{(GameServiceType)i}] ");
if (_serviceThreads[i] != null)
sb.AppendLine($"{_services[i].GetStatus()}");
else
sb.AppendLine("Not running");
}
if (includeMetrics)
{
sb.AppendLine("Performance Metrics:");
sb.AppendLine(MetricsManager.Instance.GeneratePerformanceReport(MetricsReportFormat.PlainText));
}
foreach (var kvp in statusDict)
sb.AppendLine($"{kvp.Key}: {kvp.Value}");
return sb.ToString();
}

View file

@ -3,7 +3,7 @@
/// <summary>
/// Exposes a packet's serialization routine.
/// </summary>
public interface IPacket
public interface IPacket : IDisposable
{
public int SerializedSize { get; }

View file

@ -1,7 +1,7 @@
using System.Net;
using System.Net.Sockets;
using MHServerEmu.Core.Config;
using MHServerEmu.Core.Extensions;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Network.Tcp
@ -37,10 +37,13 @@ namespace MHServerEmu.Core.Network.Tcp
public override string ToString()
{
if (HideSensitiveInformation)
return RemoteEndPoint?.ToStringMasked();
if (RemoteEndPoint == null)
return "NULL";
return RemoteEndPoint?.ToString();
if (HideSensitiveInformation)
return $"0x{HashHelper.Djb2(RemoteEndPoint.Address.ToString()):X8}";
return RemoteEndPoint.ToString();
}
/// <summary>

View file

@ -345,13 +345,22 @@ namespace MHServerEmu.Core.Network.Tcp
/// </summary>
private async Task<int> SendAsync<T>(TcpClientConnection connection, T packet, SocketFlags flags = SocketFlags.None) where T: IPacket
{
int sent = 0;
int size = packet.SerializedSize;
byte[] buffer = _bufferPool.Rent(size);
packet.Serialize(buffer);
int sent = await SendAsync(connection, buffer, size, flags);
try
{
packet.Serialize(buffer);
sent = await SendAsync(connection, buffer, size, flags);
}
finally
{
_bufferPool.Return(buffer);
packet.Dispose();
}
_bufferPool.Return(buffer);
return sent;
}

View file

@ -0,0 +1,21 @@
namespace MHServerEmu.Core.Network.Web
{
public enum WebApiAccessType
{
None,
AccountManagement,
/*
* Add more access types here as needed.
*
* Do not change the order of existing types because they are saved to disk,
* so changing the order can change access for previously saved keys.
*
* If you want to add access types for your custom functionality, start with
* a higher base value (e.g. 100000) to avoid conflicts with potential future
* built-in access types.
*/
NumTypes,
}
}

View file

@ -0,0 +1,117 @@
using System.Text.Json;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Memory;
using MHServerEmu.Core.System;
namespace MHServerEmu.Core.Network.Web
{
public enum WebApiKeyVerificationResult
{
Success,
InvalidKey,
KeyNotFound,
AccessMismatch,
}
/// <summary>
/// Singleton implementation of <see cref="TokenManager{T}"/> for managing <see cref="WebApiKeyData"/> instances.
/// </summary>
public class WebApiKeyManager
{
private static readonly string KeyFilePath = Path.Combine(FileHelper.DataDirectory, "Web", "ApiKeys.json");
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly TokenManager<WebApiKeyData> _keys = new();
public static WebApiKeyManager Instance { get; } = new();
private WebApiKeyManager() { }
public void LoadKeys()
{
_keys.Clear();
if (File.Exists(KeyFilePath) == false)
return;
var keys = FileHelper.DeserializeJson<List<KeyValuePair<string, WebApiKeyData>>>(KeyFilePath);
if (keys == null)
{
Logger.Warn("LoadKeys(): Failed to deserialize web API keys");
return;
}
foreach (var kvp in keys)
{
string key = kvp.Key;
WebApiKeyData keyData = kvp.Value;
if (_keys.ImportToken(key, keyData) == false)
{
Logger.Warn($"LoadKeys(): Failed to import web API key [{keyData}]");
continue;
}
Logger.Info($"Loaded web API key [{keyData}]");
}
}
public void SaveKeys()
{
using var keysHandle = ListPool<KeyValuePair<string, WebApiKeyData>>.Instance.Get(out var keys);
_keys.ExportTokens(keys);
FileHelper.SerializeJson(KeyFilePath, keys, JsonOptions);
}
public string CreateKey(string name, WebApiAccessType access)
{
string key = null;
if (string.IsNullOrWhiteSpace(name))
return Logger.WarnReturn(key, $"CreateKey(): Invalid key name '{name}'");
if (access <= WebApiAccessType.None || access >= WebApiAccessType.NumTypes)
return Logger.WarnReturn(key, $"CreateKey(): Invalid access type {access}");
WebApiKeyData keyData = new(name, access, DateTime.UtcNow);
key = _keys.GenerateToken(keyData);
SaveKeys();
return key;
}
public WebApiKeyVerificationResult VerifyKey(string key, WebApiAccessType requiredAccess, out string keyName)
{
keyName = string.Empty;
if (string.IsNullOrWhiteSpace(key))
return WebApiKeyVerificationResult.InvalidKey;
if (_keys.TryGetValue(key, out WebApiKeyData keyData) == false)
return WebApiKeyVerificationResult.KeyNotFound;
if (keyData.Access != requiredAccess)
return WebApiKeyVerificationResult.AccessMismatch;
keyName = keyData.Name;
return WebApiKeyVerificationResult.Success;
}
private class WebApiKeyData(string name, WebApiAccessType access, DateTime creationTime)
{
public string Name { get; init; } = name;
public WebApiAccessType Access { get; init; } = access;
public DateTime CreationTime { get; init; } = creationTime;
public override string ToString()
{
return $"{Name} ({Access})";
}
}
}
}

View file

@ -0,0 +1,130 @@
using System.Net;
using MHServerEmu.Core.Extensions;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Network.Web
{
public abstract class WebHandler
{
private static readonly Logger Logger = LogManager.CreateLogger();
public virtual WebApiAccessType Access { get => WebApiAccessType.None; }
public WebService Service { get; private set; }
public string LocalPath { get; private set; }
/// <summary>
/// Adds a reference to the <see cref="WebService"/> this <see cref="WebHandler"/> is registered to.
/// </summary>
internal void Register(WebService service, string localPath)
{
Service = service;
LocalPath = localPath;
}
/// <summary>
/// Removes the reference to the <see cref="WebService"/> this <see cref="WebHandler"/> is currently registered to.
/// </summary>
internal void Unregister()
{
Service = null;
LocalPath = null;
}
/// <summary>
/// Handles <see cref="WebRequestContext"/> asynchronously.
/// </summary>
internal async Task HandleAsync(WebRequestContext context)
{
try
{
if (Authorize(context) == false)
{
context.StatusCode = (int)HttpStatusCode.Forbidden;
return;
}
switch (context.HttpMethod)
{
case "GET":
await Get(context);
break;
case "POST":
await Post(context);
break;
case "DELETE":
await Delete(context);
break;
default:
await HandleMethodNotAllowed(context);
break;
}
}
catch (Exception e)
{
context.StatusCode = (int)HttpStatusCode.InternalServerError;
Logger.Warn($"Error handling {context}: {e}");
}
}
/// <summary>
/// Handles a GET request asynchronously.
/// </summary>
protected virtual Task Get(WebRequestContext context)
{
return HandleMethodNotAllowed(context);
}
/// <summary>
/// Handles a POST request asynchronously.
/// </summary>
protected virtual Task Post(WebRequestContext context)
{
return HandleMethodNotAllowed(context);
}
/// <summary>
/// Handles a DELETE request asynchronously.
/// </summary>
protected virtual Task Delete(WebRequestContext context)
{
return HandleMethodNotAllowed(context);
}
/// <summary>
/// Fallback for unsupported HTTP method requests.
/// </summary>
private static Task HandleMethodNotAllowed(WebRequestContext context)
{
Logger.Warn($"Unsupported HTTP method {context.HttpMethod} for local path {context.LocalPath}");
context.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
return Task.CompletedTask;
}
private bool Authorize(WebRequestContext context)
{
// NOTE: If we decide to add global rate limiting of some kind, this can be done here.
WebApiAccessType access = Access;
if (access == WebApiAccessType.None)
return true;
string ipAddressHandle = context.GetIPAddressHandle();
string webApiKey = context.GetBearerToken();
WebApiKeyVerificationResult result = WebApiKeyManager.Instance.VerifyKey(webApiKey, access, out string keyName);
if (result != WebApiKeyVerificationResult.Success)
{
Logger.Warn($"Authorize(): Failed to authorize request to {LocalPath} from {ipAddressHandle} using key [{keyName}], result={result}");
return false;
}
Logger.Info($"Authorized request to {LocalPath} from {ipAddressHandle} using key [{keyName}]");
return true;
}
}
}

View file

@ -0,0 +1,194 @@
using System.Buffers;
using System.Collections.Specialized;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Web;
using Google.ProtocolBuffers;
namespace MHServerEmu.Core.Network.Web
{
/// <summary>
/// Wrapper for <see cref="HttpListenerContext"/>.
/// </summary>
public readonly struct WebRequestContext
{
private readonly HttpListenerRequest _httpRequest;
private readonly HttpListenerResponse _httpResponse;
public string UserAgent { get => _httpRequest.UserAgent; }
public string LocalPath { get => _httpRequest.Url.LocalPath; }
public string HttpMethod { get => _httpRequest.HttpMethod; }
public string XForwardedFor { get => _httpRequest.Headers["X-Forwarded-For"]; }
public string Authorization { get => _httpRequest.Headers["Authorization"]; }
public bool IsGameClientRequest { get => UserAgent.Equals("Secret Identity Studios Http Client", StringComparison.InvariantCulture); }
public int StatusCode { get => _httpResponse.StatusCode; set => _httpResponse.StatusCode = value; }
public WebRequestContext(HttpListenerContext httpContext)
{
_httpRequest = httpContext.Request;
_httpResponse = httpContext.Response;
_httpResponse.StatusCode = 200;
_httpResponse.KeepAlive = false;
}
public override string ToString()
{
return $"{HttpMethod} {LocalPath}";
}
public string GetIPAddress()
{
string forwardedFor = XForwardedFor;
if (string.IsNullOrWhiteSpace(forwardedFor) == false)
return forwardedFor;
return _httpRequest.RemoteEndPoint.Address.ToString();
}
public string GetBearerToken()
{
string authorization = Authorization;
if (string.IsNullOrWhiteSpace(authorization))
return null;
string[] data = authorization.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (data == null || data.Length < 2)
return null;
if (data[0].Equals("Bearer", StringComparison.OrdinalIgnoreCase) == false)
return null;
return data[1];
}
public void Redirect(string url)
{
_httpResponse.Redirect(url);
}
/// <summary>
/// Asynchronously reads the request input stream as a UTF-8 string.
/// </summary>
public async Task<string> ReadUtf8StringAsync()
{
const long MaxLength = 1024 * 16;
int length = (int)_httpRequest.ContentLength64;
if (length < 0 || length > MaxLength)
throw new InternalBufferOverflowException();
byte[] buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
await _httpRequest.InputStream.ReadAsync(buffer.AsMemory(0, length));
return Encoding.UTF8.GetString(buffer, 0, length);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// Asynchronously reads the request input stream as <typeparamref name="T"/> serialized using JSON.
/// </summary>
public async Task<T> ReadJsonAsync<T>()
{
return await JsonSerializer.DeserializeAsync<T>(_httpRequest.InputStream);
}
/// <summary>
/// Asynchronously reads the request input stream as a <see cref="NameValueCollection"/>.
/// </summary>
public async Task<NameValueCollection> ReadQueryStringAsync()
{
string queryString = await ReadUtf8StringAsync();
return HttpUtility.ParseQueryString(queryString);
}
/// <summary>
/// Reads the request input stream as an <see cref="IMessage"/> of protocol <typeparamref name="T"/>.
/// </summary>
public IMessage ReadProtobuf<T>() where T: Enum
{
MessageBuffer messageBuffer = new(_httpRequest.InputStream);
return messageBuffer.Deserialize<T>();
}
/// <summary>
/// Asynchronously responds to the request with the provided payload.
/// </summary>
public async Task SendAsync(byte[] payload, string contentType)
{
_httpResponse.ContentType = contentType;
_httpResponse.ContentLength64 = payload.Length;
await _httpResponse.OutputStream.WriteAsync(payload);
}
/// <summary>
/// Asynchronously responds to the request with the provided <see cref="string"/> encoded as UTF-8.
/// </summary>
public async Task SendAsync(string message, string contentType = "text/plain")
{
int maxByteCount = Encoding.UTF8.GetMaxByteCount(message.Length);
byte[] buffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
try
{
int byteCount = Encoding.UTF8.GetBytes(message, 0, message.Length, buffer, 0);
_httpResponse.ContentType = contentType;
_httpResponse.ContentLength64 = byteCount;
await _httpResponse.OutputStream.WriteAsync(buffer.AsMemory(0, byteCount));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// Asynchronously responds to the request with the provided <see cref="IMessage"/>.
/// </summary>
public async Task SendAsync(IMessage message)
{
MessagePackageOut payload = new(message);
int size = payload.GetSerializedSize();
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
CodedOutputStream cos = CodedOutputStream.CreateInstance(buffer);
payload.WriteTo(cos);
cos.Flush();
_httpResponse.ContentType = "application/octet-stream";
_httpResponse.ContentLength64 = size;
await _httpResponse.OutputStream.WriteAsync(buffer.AsMemory(0, size));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// Asynchronously responds to the request with the provided <typeparamref name="T"/> instance serialized to JSON.
/// </summary>
public async Task SendJsonAsync<T>(T @object)
{
_httpResponse.ContentType = "application/json";
await JsonSerializer.SerializeAsync(_httpResponse.OutputStream, @object);
}
}
}

View file

@ -0,0 +1,158 @@
using System.Diagnostics;
using System.Net;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Network.Web
{
public class WebService
{
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly Dictionary<string, WebHandler> _handlers = new(StringComparer.OrdinalIgnoreCase);
private HttpListener _listener;
private CancellationTokenSource _cts;
public WebServiceSettings Settings { get; }
public bool IsRunning { get; private set; }
public int HandlerCount { get => _handlers.Count; }
public int HandledRequests { get; private set; }
public WebService(WebServiceSettings settings)
{
Settings = settings;
}
public override string ToString()
{
return Settings.Name;
}
/// <summary>
/// Starts the web service. Returns <see langword="true"/> if successful.
/// </summary>
public bool Start()
{
if (IsRunning)
return false;
Debug.Assert(_listener == null);
Debug.Assert(_cts == null);
string url = Settings.ListenUrl;
_listener = new();
_listener.Prefixes.Add(url);
_listener.Start();
_cts = new();
Task.Run(HandleRequestsAsync);
IsRunning = true;
return true;
}
/// <summary>
/// Stops the currently running REST service. Returns <see langword="true"/> if successful.
/// </summary>
public bool Stop()
{
if (IsRunning == false)
return false;
Debug.Assert(_listener != null);
Debug.Assert(_cts != null);
_cts.Cancel();
_cts.Dispose();
_cts = null;
_listener.Stop();
_listener = null;
IsRunning = false;
return true;
}
/// <summary>
/// Returns the currently registered <see cref="WebHandler"/> for the specified local path if available.
/// Returns the fallback handler if no handler is registered for the local path, which may be <see langword="null"/>.
/// </summary>
public WebHandler GetHandler(string localPath)
{
if (_handlers.TryGetValue(localPath, out WebHandler handler) == false)
return Settings.FallbackHandler;
return handler;
}
/// <summary>
/// Registers the provided <see cref="WebHandler"/> for the specified local path.
/// Returns <see langword="true"/> if successful.
/// </summary>
public bool RegisterHandler(string localPath, WebHandler handler)
{
bool added = _handlers.TryAdd(localPath, handler);
if (added)
handler.Register(this, localPath);
else
Logger.Warn($"RegisterHandler(): Local path {localPath} already has a registered handler");
return added;
}
/// <summary>
/// Removed the currently registered <see cref="WebHandler"/> for the specified local path.
/// Returns <see langword="true"/> if successful.
/// </summary>
public bool RemoveHandler(string localPath)
{
bool removed = _handlers.Remove(localPath, out WebHandler handler);
if (removed)
handler.Unregister();
else
Logger.Warn($"RemoveHandler(): No handler is registered for local path {localPath}");
return removed;
}
/// <summary>
/// Handles incoming requests asynchronously.
/// </summary>
private async Task HandleRequestsAsync()
{
Logger.Info($"{this} is listening on {Settings.ListenUrl}...");
while (_cts.IsCancellationRequested == false)
{
try
{
HttpListenerContext httpContext = await _listener.GetContextAsync().WaitAsync(_cts.Token);
WebRequestContext requestContext = new(httpContext);
// This may be either a registered handler or a fallback handler.
WebHandler handler = GetHandler(requestContext.LocalPath);
await handler?.HandleAsync(requestContext);
httpContext.Response.Close();
HandledRequests++;
}
catch (TaskCanceledException)
{
return;
}
catch (Exception e)
{
// NOTE: HandleRequest() should catch and handle exceptions when processing requests.
// If we got to this part, something must be wrong with the listener.
Logger.Error($"HandleRequestAsync(): {e}");
return;
}
}
}
}
}

View file

@ -0,0 +1,9 @@
namespace MHServerEmu.Core.Network.Web
{
public class WebServiceSettings
{
public string Name { get; init; }
public string ListenUrl { get; init; }
public WebHandler FallbackHandler { get; init; }
}
}

View file

@ -1,7 +1,7 @@
using System.Diagnostics;
using MHServerEmu.Core.System.Time;
namespace MHServerEmu.Core.System
namespace MHServerEmu.Core.RateLimiting
{
public class TimeLeakyBucketCollection<TKey>
{
@ -25,11 +25,11 @@ namespace MHServerEmu.Core.System
_dict.TryGetValue(key, out TimeSpan time);
TimeSpan currentTime = Clock.ElapsedTime;
if ((currentTime - time) > TimeSpan.Zero)
if (currentTime - time > TimeSpan.Zero)
time = currentTime;
TimeSpan newTime = time + _cost;
if ((newTime - currentTime) >= _maxCost)
if (newTime - currentTime >= _maxCost)
{
_dict[key] = time;
return false;

View file

@ -1,7 +1,7 @@
using System.Diagnostics;
using MHServerEmu.Core.System.Time;
namespace MHServerEmu.Core.System
namespace MHServerEmu.Core.RateLimiting
{
/// <summary>
/// A rate limiter based on the token bucket algorithm.
@ -26,6 +26,7 @@ namespace MHServerEmu.Core.System
_maxTokens = maxTokens;
_currentTokens = _maxTokens;
_lastRefillTime = Clock.ElapsedTime;
}
/// <summary>
@ -51,10 +52,10 @@ namespace MHServerEmu.Core.System
TimeSpan currentTime = Clock.ElapsedTime;
TimeSpan elapsed = currentTime - _lastRefillTime;
int tokensGained = (int)(elapsed.Ticks / _ticksPerToken);
long tokensGained = elapsed.Ticks / _ticksPerToken;
if (tokensGained > 0)
{
_currentTokens = Math.Min(_currentTokens + tokensGained, _maxTokens);
_currentTokens = (int)Math.Clamp(_currentTokens + tokensGained, 0, _maxTokens);
_lastRefillTime = currentTime;
}
}

View file

@ -19,38 +19,48 @@ namespace MHServerEmu.Core.Serialization
Disk = 4 // Server <-> File
}
// We are most likely not going to have as much versioning as the original game,
// so we will use ArchiveVersion for all archive versioning. At some point when
// things get more stable we may want to clear this and force a wipe of everything.
public enum ArchiveVersion : uint
{
Invalid = 0,
Initial = 1,
AddedMissions = 2,
AddedVendorPurchaseData = 3,
ImplementedConditionPersistence = 4,
ImplementedLoginRewards = 5,
ImplementedMapDiscoveryDataPersistence = 6,
AddedRegionProtoRefToMapDiscoveryData = 7,
// Versions 1-8 were used in the 0.x branch, so we start at 9 here.
Initial = 9,
// Update the current version if you add any <---------
Current = AddedRegionProtoRefToMapDiscoveryData
Current = Initial
}
public enum GameBuildNumber : uint
{
Invalid = 0,
// Changelist is the most consistent single number we have to identify different builds of the game, so use it for persistent archives.
// Add more changelist numbers here for any other versions of the game we are going to support.
_1_10_0_69 = 324,
_1_10_0_643 = 16688,
_1_48_0_1618 = 380454,
_1_48_0_1712 = 391562,
_1_52_0_1700 = 479899,
_1_53_0_203 = 493640,
Current = _1_52_0_1700
}
/// <summary>
/// An implementation of the custom Gazillion serialization archive format.
/// </summary>
public class Archive : IDisposable
public sealed class Archive : IDisposable
{
private static readonly Logger Logger = LogManager.CreateLogger();
// Reuse the same buffers for all archives on the same thread. In practice this means one buffer instance of each type per game.
// Reuse the same buffers for all archives on the same game thread.
[ThreadStatic]
private static byte[] ReadBuffer;
[ThreadStatic]
private static byte[] WriteBuffer;
[ThreadStatic]
private static MemoryStream SharedAutoBuffer;
[ThreadStatic]
private static byte[] CodedOutputStreamBuffer;
private readonly MemoryStream _bufferStream; // MemoryStream replaces AutoBuffer from the original implementation
private readonly MemoryStream _buffer; // MemoryStream replaces StreamAutoBuffer from the original implementation
// C# coded stream implementation is buffered, so we have to use the same stream for the whole archive
private readonly CodedOutputStream _cos;
@ -108,20 +118,15 @@ namespace MHServerEmu.Core.Serialization
if ((serializeType == ArchiveSerializeType.Replication || serializeType == ArchiveSerializeType.Database) == false)
throw new NotImplementedException($"Unsupported archive serialize type {serializeType}.");
// Initialize new buffers if this is being called for the first time on this thread.
if (SharedAutoBuffer == null)
{
SharedAutoBuffer = new(1024);
CodedOutputStreamBuffer = new byte[32]; // We flush after every value, so we can use very small buffer sizes (default is 4096).
}
InitializeBuffers();
// Reuse the same stream for all packing archives
_bufferStream = SharedAutoBuffer;
if (_bufferStream.Length > 0)
_bufferStream.SetLength(0);
_buffer = SharedAutoBuffer;
if (_buffer.Length > 0)
_buffer.SetLength(0);
// Use reflection hackery to reuse the same buffer for all coded output streams, see ProtobufHelper for details.
_cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(_bufferStream, CodedOutputStreamBuffer);
_cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(_buffer, WriteBuffer);
SerializeType = serializeType;
ReplicationPolicy = replicationPolicy;
@ -139,8 +144,10 @@ namespace MHServerEmu.Core.Serialization
if ((serializeType == ArchiveSerializeType.Replication || serializeType == ArchiveSerializeType.Database) == false)
throw new NotImplementedException($"Unsupported archive serialize type {serializeType}.");
_bufferStream = new(buffer);
_cis = CodedInputStream.CreateInstance(_bufferStream);
InitializeBuffers();
_buffer = new(buffer);
_cis = CodedInputStream.CreateInstance(_buffer, ReadBuffer);
SerializeType = serializeType;
IsPacking = false;
@ -157,13 +164,34 @@ namespace MHServerEmu.Core.Serialization
// We use ByteString.Unsafe here to avoid copying data one extra time (ByteString -> Stream instead of ByteString -> Buffer -> Stream).
}
private static void InitializeBuffers()
{
// Initialize new buffers if this is being called for the first time on this thread.
// NOTE: This approach prevents us from doing recursive writing of multiple archives on the same thread.
// In practice this is not a limitation that affects us, but it is something to keep in mind.
if (ReadBuffer == null || WriteBuffer == null || SharedAutoBuffer == null)
{
ReadBuffer = new byte[4096];
WriteBuffer = new byte[32]; // We flush after every value, so we can use very small buffer sizes for output (default is 4096).
SharedAutoBuffer = new(65536);
}
}
public void Dispose()
{
_buffer.SetLength(0);
}
/// <summary>
/// Returns the <see cref="MemoryStream"/> instance that acts as the AutoBuffer for this <see cref="Archive"/>.
/// </summary>
/// <remarks>
/// AutoBuffer is the name of the data structure that backs archives in the client.
/// </remarks>
public MemoryStream AccessAutoBuffer() => _bufferStream;
public MemoryStream AccessAutoBuffer()
{
return _buffer;
}
/// <summary>
/// Converts the underlying <see cref="MemoryStream"/> to <see cref="ByteString"/>.
@ -171,7 +199,13 @@ namespace MHServerEmu.Core.Serialization
public ByteString ToByteString()
{
// We use ByteString.Unsafe here to avoid copying data one extra time (Stream -> ByteString instead of Stream -> Buffer -> ByteString).
return ByteString.Unsafe.FromBytes(_bufferStream.ToArray());
return ByteString.Unsafe.FromBytes(_buffer.ToArray());
}
public Span<byte> AsSpan()
{
byte[] buffer = _buffer.GetBuffer();
return buffer.AsSpan(0, (int)CurrentOffset);
}
/// <summary>
@ -183,8 +217,14 @@ namespace MHServerEmu.Core.Serialization
if (IsPersistent)
{
// Write archive size placeholder that will be updated via UpdateSizeInArchive() when other data is written.
WriteUnencodedStream(0u);
uint version = (uint)Version;
success &= Transfer(ref version);
uint gameBuildNumber = (uint)GameBuildNumber.Current;
success &= Transfer(ref gameBuildNumber);
}
else if (IsReplication)
{
@ -204,9 +244,24 @@ namespace MHServerEmu.Core.Serialization
if (IsPersistent)
{
uint sizeUsed = 0;
success &= ReadUnencodedStream(ref sizeUsed);
if (sizeUsed != _buffer.Length)
{
SetError("Buffer size mismatch!");
return false;
}
uint version = 0;
success &= Transfer(ref version);
Version = (ArchiveVersion)version;
// For now just log a warning if there is a game build mismatch, in the future we can use this for migration between versions.
uint gameBuildNumber = 0;
success &= Transfer(ref gameBuildNumber);
if (gameBuildNumber != (uint)GameBuildNumber.Current)
Logger.Warn($"Game build number mismatch: expected {(uint)GameBuildNumber.Current}, got {gameBuildNumber}");
}
else if (IsReplication)
{
@ -253,7 +308,7 @@ namespace MHServerEmu.Core.Serialization
}
else
{
_bufferStream.WriteByteAt(_lastBitEncodedOffset, bitBuffer);
_buffer.WriteByteAt(_lastBitEncodedOffset, bitBuffer);
if (numEncodedBits >= 5)
_lastBitEncodedOffset = 0;
}
@ -606,7 +661,8 @@ namespace MHServerEmu.Core.Serialization
if (IsPacking)
{
return WriteVarint(ioData);
WriteVarint(ioData);
UpdateSizeInArchive();
}
else
{
@ -633,7 +689,8 @@ namespace MHServerEmu.Core.Serialization
if (IsPacking)
{
return WriteVarint(ioData);
WriteVarint(ioData);
UpdateSizeInArchive();
}
else
{
@ -675,7 +732,7 @@ namespace MHServerEmu.Core.Serialization
/// </summary>
private bool StartSizeChecking(ref long startPosition, ref uint size)
{
if (IsPersistent == false || Version < ArchiveVersion.AddedMissions)
if (IsPersistent == false)
return true;
// NOTE: COS/CIS are buffered, so we need to use their position, and not the one from the underlying stream.
@ -701,7 +758,7 @@ namespace MHServerEmu.Core.Serialization
/// </summary>
private bool EndSizeChecking(ref long startPosition, ref uint size, bool skip)
{
if (IsPersistent == false || Version < ArchiveVersion.AddedMissions)
if (IsPersistent == false)
return true;
if (IsPacking)
@ -729,6 +786,40 @@ namespace MHServerEmu.Core.Serialization
return true;
}
private bool UpdateSizeInArchive()
{
if (IsPersistent == false)
return false;
if (IsPacking == false)
{
SetError("Cant use on unpack!");
return false;
}
Span<uint> sizeToken = GetSizeTokenAtOffset(0);
if (sizeToken.Length == 0)
{
SetError("Error accessing size in the buffer!");
return false;
}
sizeToken[0] = (uint)CurrentOffset;
return true;
}
private Span<uint> GetSizeTokenAtOffset(uint offset)
{
if (CurrentOffset < offset + sizeof(uint))
{
SetError("Failed writing size in use!");
return default;
}
Span<byte> sizeToken = new(_buffer.GetBuffer(), (int)offset, sizeof(uint));
return MemoryMarshal.Cast<byte, uint>(sizeToken);
}
#endregion
#region Stream IO
@ -740,6 +831,9 @@ namespace MHServerEmu.Core.Serialization
{
_cos.WriteRawByte(value);
_cos.Flush();
UpdateSizeInArchive();
return true;
}
@ -770,6 +864,9 @@ namespace MHServerEmu.Core.Serialization
_cos.WriteRawByte(@byte);
_cos.Flush();
UpdateSizeInArchive();
return true;
}
@ -806,7 +903,7 @@ namespace MHServerEmu.Core.Serialization
// NOTE: PropertyCollection::serializeWithDefault() manipulates the archive buffer directly. First it allocates 4 bytes
// for the number of properties, than it writes all the properties, and then it goes back and updates the number.
// NOTE2: Persistent archives also do this for all ISerialize objects, except it writes the number of bytes written.
return _bufferStream.WriteUInt32At(position, value);
return _buffer.WriteUInt32At(position, value);
}
/// <summary>
@ -921,7 +1018,7 @@ namespace MHServerEmu.Core.Serialization
{
if (_lastBitEncodedOffset == 0) return null;
if (_bufferStream.ReadByteAt(_lastBitEncodedOffset, out byte lastBitEncoded) == false)
if (_buffer.ReadByteAt(_lastBitEncodedOffset, out byte lastBitEncoded) == false)
{
SetError("Failed getting last bit encoded!");
return null;
@ -978,31 +1075,5 @@ namespace MHServerEmu.Core.Serialization
}
#endregion
#region IDisposable Implementation
private bool _isDisposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed) return;
if (disposing)
{
// Not sure if we even still need IDisposable for archives with reusable streams,
// we can just rely on doing cleanup after the previous use in the constructor.
_bufferStream.SetLength(0);
}
_isDisposed = true;
}
#endregion
}
}

View file

@ -0,0 +1,18 @@
using Google.ProtocolBuffers;
namespace MHServerEmu.Core.Serialization
{
/// <summary>
/// Extended version of <see cref="ICodedOutputStream"/> that exposes additional low level writing functionality.
/// </summary>
public interface ICodedOutputStreamEx : ICodedOutputStream
{
void WriteRawVarint32(uint value);
void WriteRawVarint64(ulong value);
void WriteRawByte(byte value);
void WriteRawBytes(byte[] value);
}
}

View file

@ -0,0 +1,459 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Google.ProtocolBuffers;
using Google.ProtocolBuffers.Descriptors;
using MHServerEmu.Core.Helpers;
namespace MHServerEmu.Core.Serialization
{
/// <summary>
/// A more memory efficient version of <see cref="CodedOutputStream"/>.
/// </summary>
public sealed class RecyclableCodedOutputStream : ICodedOutputStreamEx, IDisposable
{
private static readonly ConcurrentBag<RecyclableCodedOutputStream> Instances = new();
private readonly byte[] _primaryBuffer = new byte[CodedOutputStream.DefaultBufferSize];
private readonly byte[] _floatBuffer = new byte[sizeof(float)];
private CodedOutputStream _cos;
private RecyclableCodedOutputStream() { }
private void Initialize(Stream stream)
{
_cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(stream, _primaryBuffer);
}
public static RecyclableCodedOutputStream CreateInstance(Stream stream)
{
if (Instances.TryTake(out RecyclableCodedOutputStream cos) == false)
cos = new();
cos.Initialize(stream);
return cos;
}
#region IDisposable
public void Dispose()
{
if (_cos != null)
{
_cos.Flush();
_cos = null;
}
Instances.Add(this);
}
#endregion
#region ICodedOutputStream
// For most of this we just pass everything to the default implementation.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Flush()
{
_cos.Flush();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteArray(FieldType fieldType, int fieldNumber, string fieldName, IEnumerable list)
{
_cos.WriteArray(fieldType, fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBool(int fieldNumber, string fieldName, bool value)
{
_cos.WriteBool(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBoolArray(int fieldNumber, string fieldName, IEnumerable<bool> list)
{
_cos.WriteBoolArray(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytes(int fieldNumber, string fieldName, ByteString value)
{
_cos.WriteBytes(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytesArray(int fieldNumber, string fieldName, IEnumerable<ByteString> list)
{
_cos.WriteBytesArray(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDouble(int fieldNumber, string fieldName, double value)
{
_cos.WriteDouble(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDoubleArray(int fieldNumber, string fieldName, IEnumerable<double> list)
{
_cos.WriteDoubleArray(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteEnum(int fieldNumber, string fieldName, int value, object rawValue)
{
_cos.WriteEnum(fieldNumber, fieldName, value, rawValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteEnumArray<T>(int fieldNumber, string fieldName, IEnumerable<T> list) where T : struct, IComparable, IFormattable
{
_cos.WriteEnumArray(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteField(FieldType fieldType, int fieldNumber, string fieldName, object value)
{
_cos.WriteField(fieldType, fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixed32(int fieldNumber, string fieldName, uint value)
{
_cos.WriteFixed32(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixed32Array(int fieldNumber, string fieldName, IEnumerable<uint> list)
{
_cos.WriteFixed32Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixed64(int fieldNumber, string fieldName, ulong value)
{
_cos.WriteFixed64(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixed64Array(int fieldNumber, string fieldName, IEnumerable<ulong> list)
{
_cos.WriteFixed64Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFloat(int fieldNumber, string fieldName, float value)
{
//_cos.WriteFloat(fieldNumber, fieldName, value);
_cos.WriteTag(fieldNumber, WireFormat.WireType.Fixed32);
MemoryMarshal.Cast<byte, float>(_floatBuffer)[0] = value;
_cos.WriteRawBytes(_floatBuffer, 0, 4);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFloatArray(int fieldNumber, string fieldName, IEnumerable<float> list)
{
_cos.WriteFloatArray(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGroup(int fieldNumber, string fieldName, IMessageLite value)
{
_cos.WriteGroup(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGroupArray<T>(int fieldNumber, string fieldName, IEnumerable<T> list) where T : IMessageLite
{
_cos.WriteGroupArray(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt32(int fieldNumber, string fieldName, int value)
{
_cos.WriteInt32(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt32Array(int fieldNumber, string fieldName, IEnumerable<int> list)
{
_cos.WriteInt32Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt64(int fieldNumber, string fieldName, long value)
{
_cos.WriteInt64(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt64Array(int fieldNumber, string fieldName, IEnumerable<long> list)
{
_cos.WriteInt64Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteMessage(int fieldNumber, string fieldName, IMessageLite value)
{
_cos.WriteMessage(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteMessageArray<T>(int fieldNumber, string fieldName, IEnumerable<T> list) where T : IMessageLite
{
_cos.WriteMessageArray(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteMessageEnd()
{
_cos.Flush();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteMessageSetExtension(int fieldNumber, string fieldName, IMessageLite value)
{
_cos.WriteMessageSetExtension(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteMessageSetExtension(int fieldNumber, string fieldName, ByteString value)
{
_cos.WriteMessageSetExtension(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteMessageStart()
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedArray(FieldType fieldType, int fieldNumber, string fieldName, IEnumerable list)
{
_cos.WritePackedArray(fieldType, fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedBoolArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<bool> list)
{
_cos.WritePackedBoolArray(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedDoubleArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<double> list)
{
_cos.WritePackedDoubleArray(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedEnumArray<T>(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<T> list) where T : struct, IComparable, IFormattable
{
_cos.WritePackedEnumArray(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedFixed32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<uint> list)
{
_cos.WritePackedFixed32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedFixed64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<ulong> list)
{
_cos.WritePackedFixed64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedFloatArray(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<float> list)
{
_cos.WritePackedFloatArray(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
{
_cos.WritePackedInt32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
{
_cos.WritePackedInt64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedSFixed32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
{
_cos.WritePackedSFixed32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedSFixed64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
{
_cos.WritePackedSFixed64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedSInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<int> list)
{
_cos.WritePackedSInt32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedSInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<long> list)
{
_cos.WritePackedSInt64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedUInt32Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<uint> list)
{
_cos.WritePackedUInt32Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePackedUInt64Array(int fieldNumber, string fieldName, int calculatedSize, IEnumerable<ulong> list)
{
_cos.WritePackedUInt64Array(fieldNumber, fieldName, calculatedSize, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSFixed32(int fieldNumber, string fieldName, int value)
{
_cos.WriteSFixed32(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSFixed32Array(int fieldNumber, string fieldName, IEnumerable<int> list)
{
_cos.WriteSFixed32Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSFixed64(int fieldNumber, string fieldName, long value)
{
_cos.WriteSFixed64(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSFixed64Array(int fieldNumber, string fieldName, IEnumerable<long> list)
{
_cos.WriteSFixed64Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSInt32(int fieldNumber, string fieldName, int value)
{
_cos.WriteSInt32(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSInt32Array(int fieldNumber, string fieldName, IEnumerable<int> list)
{
_cos.WriteSInt32Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSInt64(int fieldNumber, string fieldName, long value)
{
_cos.WriteSInt64(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteSInt64Array(int fieldNumber, string fieldName, IEnumerable<long> list)
{
_cos.WriteSInt64Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteString(int fieldNumber, string fieldName, string value)
{
_cos.WriteString(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteStringArray(int fieldNumber, string fieldName, IEnumerable<string> list)
{
_cos.WriteStringArray(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUInt32(int fieldNumber, string fieldName, uint value)
{
_cos.WriteUInt32(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUInt32Array(int fieldNumber, string fieldName, IEnumerable<uint> list)
{
_cos.WriteUInt32Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUInt64(int fieldNumber, string fieldName, ulong value)
{
_cos.WriteUInt64(fieldNumber, fieldName, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUInt64Array(int fieldNumber, string fieldName, IEnumerable<ulong> list)
{
_cos.WriteUInt64Array(fieldNumber, fieldName, list);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUnknownBytes(int fieldNumber, ByteString value)
{
_cos.WriteUnknownBytes(fieldNumber, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUnknownField(int fieldNumber, WireFormat.WireType wireType, ulong value)
{
_cos.WriteUnknownField(fieldNumber, wireType, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[Obsolete]
public void WriteUnknownGroup(int fieldNumber, IMessageLite value)
{
_cos.WriteUnknownGroup(fieldNumber, value);
}
#endregion
#region ICodedOutputStreamEx
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRawVarint32(uint value)
{
_cos.WriteRawVarint32(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRawVarint64(ulong value)
{
_cos.WriteRawVarint64(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRawByte(byte value)
{
_cos.WriteRawByte(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRawBytes(byte[] value)
{
_cos.WriteRawBytes(value);
}
#endregion
}
}

View file

@ -1,4 +1,6 @@
namespace MHServerEmu.Core.System.Random
using MHServerEmu.Core.System.Time;
namespace MHServerEmu.Core.System.Random
{
// More info on MWC random: https://en.wikipedia.org/wiki/Multiply-with-carry_pseudorandom_number_generator
public class RandMwc
@ -8,7 +10,7 @@
public RandMwc(uint seed)
{
SetSeed(seed == 0 ? (uint)DateTime.Now.Ticks : seed);
SetSeed(seed == 0 ? (uint)Clock.UtcNowPrecise.Ticks : seed);
}
public void SetSeed(uint seed)

View file

@ -0,0 +1,71 @@
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Memory;
namespace MHServerEmu.Core.System.Time
{
/// <summary>
/// A timed event scheduler implementation intended to be used in game services where performance and precise timing is not as important.
/// </summary>
public class ServiceEventScheduler<THandle, TEventData>
{
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly Dictionary<THandle, ServiceEvent> _events = new();
public ServiceEventScheduler()
{
}
public void TriggerEvents()
{
if (_events.Count == 0)
return;
TimeSpan now = Clock.UnixTime;
using var eventsHandle = ListPool<ServiceEvent>.Instance.Get(out List<ServiceEvent> events);
events.AddRange(_events.Values);
foreach (ServiceEvent serviceEvent in events)
{
if (now < serviceEvent.FireTime)
continue;
// Important: remove before triggering, because triggering can schedule another event with the same handle.
_events.Remove(serviceEvent.Handle);
serviceEvent.Trigger();
}
}
public bool ScheduleEvent(THandle handle, TimeSpan delay, Action<TEventData> callback, TEventData eventData = default)
{
if (callback == null) return Logger.WarnReturn(false, "ScheduleEvent(): callback == null");
CancelEvent(handle);
TimeSpan fireTime = Clock.UnixTime + delay;
ServiceEvent @event = new(handle, fireTime, callback, eventData);
_events.Add(handle, @event);
return true;
}
public bool CancelEvent(THandle handle)
{
return _events.Remove(handle);
}
private readonly struct ServiceEvent(THandle handle, TimeSpan fireTime, Action<TEventData> callback, TEventData eventData)
{
public readonly THandle Handle = handle;
public readonly TimeSpan FireTime = fireTime;
public readonly Action<TEventData> Callback = callback;
public readonly TEventData EventData = eventData;
public void Trigger()
{
Callback(EventData);
}
}
}
}

View file

@ -0,0 +1,93 @@
using System.Security.Cryptography;
namespace MHServerEmu.Core.System
{
/// <summary>
/// Generates random cryptographic tokens bound to <typeparamref name="T"/> values for accessing restricted APIs. This class is thread-safe.
/// </summary>
public class TokenManager<T>
{
private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
private readonly Dictionary<string, T> _lookup = new();
private readonly byte[] _buffer;
public int TokenSize { get => _buffer.Length; }
public int Count { get => _lookup.Count; }
/// <summary>
/// Constructs a new <see cref="TokenManager{T}"/> with the specified token size in bytes.
/// </summary>
public TokenManager(int tokenSize = 16)
{
_buffer = new byte[tokenSize];
}
/// <summary>
/// Generates a new token for the provided <typeparamref name="T"/> value.
/// </summary>
public string GenerateToken(T lookupValue)
{
lock (_lookup)
{
string token;
// The probability of generating the same token should be low, but it's still possible in theory.
do
{
_rng.GetBytes(_buffer);
token = Convert.ToHexString(_buffer);
}
while (_lookup.TryAdd(token, lookupValue) == false);
Array.Clear(_buffer);
return token;
}
}
/// <summary>
/// Removes the provided token. Returns <see langword="true"/> if successful.
/// </summary>
public bool RemoveToken(string token)
{
lock (_lookup)
return _lookup.Remove(token);
}
/// <summary>
/// Clears all previously added tokens.
/// </summary>
public void Clear()
{
lock (_lookup)
_lookup.Clear();
}
/// <summary>
/// Adds an existing token to this manager. Returns <see cref="true"/> if successful.
/// </summary>
public bool ImportToken(string token, T value)
{
lock (_lookup)
return _lookup.TryAdd(token, value);
}
/// <summary>
/// Copies tokens to the provided <see cref="List{T}"/>.
/// </summary>
public void ExportTokens(List<KeyValuePair<string, T>> tokens)
{
lock (_lookup)
tokens.AddRange(_lookup);
}
/// <summary>
/// Retrieves the <typeparamref name="T"/> value associated with the provided token. Returns <see langword="true"/> if successful.
/// </summary>
public bool TryGetValue(string token, out T value)
{
lock (_lookup)
return _lookup.TryGetValue(token, out value);
}
}
}

View file

@ -0,0 +1,98 @@
namespace MHServerEmu.Core.Threading
{
/// <summary>
/// Manages awaitable custom <see cref="Task{TResult}"/> instances that can be manually completed.
/// </summary>
public class TaskManager<T>
{
private readonly Dictionary<ulong, TaskCompletionSource<T>> _pendingTasks = new();
private ulong _currentTaskId = 1;
/// <summary>
/// Constructs a new <see cref="Task{TResult}"/>.
/// </summary>
public Handle CreateTask()
{
lock (_pendingTasks)
{
ulong taskId = _currentTaskId++;
TaskCompletionSource<T> tcs = new();
_pendingTasks.Add(taskId, tcs);
return new(taskId, tcs.Task, this);
}
}
/// <summary>
/// Completes the <see cref="Task{TResult}"/> with the specified id using the provided result data. Returns <see langword="true"/> if successful.
/// </summary>
public bool CompleteTask(ulong taskId, T result)
{
TaskCompletionSource<T> tcs = null;
lock (_pendingTasks)
{
if (_pendingTasks.Remove(taskId, out tcs) == false)
return false;
}
tcs.SetResult(result);
return true;
}
/// <summary>
/// Cancels the <see cref="Task{TResult}"/> with the specified id. Returns <see langword="true"/> if successful.
/// </summary>
public bool CancelTask(ulong taskId)
{
TaskCompletionSource<T> tcs = null;
lock (_pendingTasks)
{
if (_pendingTasks.Remove(taskId, out tcs) == false)
return false;
}
tcs.SetCanceled();
return true;
}
/// <summary>
/// Cancels all current tasks.
/// </summary>
public void CancelAllTasks()
{
lock (_pendingTasks)
{
foreach (var kvp in _pendingTasks)
kvp.Value.SetCanceled();
_pendingTasks.Clear();
}
}
/// <summary>
/// Represents a <see cref="Task{TResult}"/> managed by a <see cref="TaskManager{T}"/>.
/// </summary>
public readonly struct Handle
{
public readonly ulong Id;
public readonly Task<T> Task;
public readonly TaskManager<T> Manager;
internal Handle(ulong id, Task<T> task, TaskManager<T> manager)
{
Id = id;
Task = task;
Manager = manager;
}
/// <summary>
/// Cancels the <see cref="Task{TResult}"/> represented by this handle.
/// </summary>
public void Cancel()
{
Manager.CancelTask(Id);
}
}
}
}

View file

@ -65,7 +65,7 @@ namespace MHServerEmu.Core.VectorMath
return Yaw == other.Yaw && Pitch == other.Pitch && Roll == other.Roll;
}
public static bool IsFinite(Orientation v)
public static bool IsFinite(ref Orientation v)
{
return float.IsFinite(v.Yaw) && float.IsFinite(v.Pitch) && float.IsFinite(v.Roll);
}

View file

@ -100,7 +100,8 @@ namespace MHServerEmu.Core.VectorMath
public static Aabb2 operator *(in Transform3 t, in Aabb2 b)
{
var points = b.GetPoints();
Span<Point2> points = stackalloc Point2[4];
b.GetPoints(points);
var box = new Aabb2();
foreach (Point2 point in points)
box.Expand(t * new Point2(point.X, point.Y));

View file

@ -38,13 +38,18 @@ namespace MHServerEmu.DatabaseAccess
/// <summary>
/// Queries the name of the player with the specified id. Returns <see langword="true"/> if successful.
/// </summary>
public bool TryGetPlayerName(ulong id, out string playerName);
public bool TryGetPlayerName(ulong playerDbId, out string playerName);
/// <summary>
/// Queries the names of all registered players from the database and adds them to the provided <see cref="Dictionary{TKey, TValue}"/>.
/// </summary>
public bool GetPlayerNames(Dictionary<ulong, string> playerNames);
/// <summary>
/// Queries last logout time for the player with the specified id. Returns <see langword="true"/> if successful.
/// </summary>
public bool TryGetLastLogoutTime(ulong playerDbId, out long lastLogoutTime);
/// <summary>
/// Inserts a new <see cref="DBAccount"/> with all of its data into the database.
/// </summary>
@ -64,5 +69,30 @@ namespace MHServerEmu.DatabaseAccess
/// Saves persistent game data stored in the database for the provided <see cref="DBAccount"/>.
/// </summary>
public bool SavePlayerData(DBAccount account);
/// <summary>
/// Loads <see cref="DBGuild"/> instances stored in the database and adds them to the provided <see cref="List{T}"/>.
/// </summary>
public bool LoadGuilds(List<DBGuild> guilds);
/// <summary>
/// Inserts or updates an existing <see cref="DBGuild"/> instance stored in the database.
/// </summary>
public bool SaveGuild(DBGuild guild);
/// <summary>
/// Deletes a stored <see cref="DBGuild"/> instance with all of its members from the database.
/// </summary>
public bool DeleteGuild(DBGuild guild);
/// <summary>
/// Inserts or updates an existing <see cref="DBGuildMember"/> instance stored in the database.
/// </summary>
public bool SaveGuildMember(DBGuildMember guildMember);
/// <summary>
/// Delets a store <see cref="DBGuildMember"/> instance from the database.
/// </summary>
public bool DeleteGuildMember(DBGuildMember guildMember);
}
}

View file

@ -0,0 +1,47 @@
using System.Text.Json;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.RateLimiting;
using MHServerEmu.DatabaseAccess.Models;
namespace MHServerEmu.DatabaseAccess.Json
{
/// <summary>
/// Serializes <see cref="DBAccount"/> instances to JSON.
/// </summary>
public class DBAccountJsonSerializer
{
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly JsonSerializerOptions _options = new();
private readonly TimeLeakyBucketCollection<ulong> _rateLimiter = new(TimeSpan.FromMinutes(30), 5);
public static DBAccountJsonSerializer Instance { get; } = new();
private DBAccountJsonSerializer()
{
_options.Converters.Add(new DBEntityCollectionJsonConverter());
}
public bool TrySerializeAccount(DBAccount account, bool checkRateLimit, out string json)
{
json = string.Empty;
if (account == null) return Logger.WarnReturn(false, "TrySerializeAccount(): account == null");
if (checkRateLimit && _rateLimiter.AddTime((ulong)account.Id) == false)
return false;
try
{
json = JsonSerializer.Serialize(account, _options);
}
catch (Exception e)
{
Logger.Error($"Failed to serialize account {account}: {e.Message}");
return false;
}
return true;
}
}
}

View file

@ -67,6 +67,7 @@ namespace MHServerEmu.DatabaseAccess.Json
public bool TryQueryAccountByEmail(string email, out DBAccount account)
{
account = _account;
account.MigrationData.Reset();
return true;
}
@ -88,6 +89,12 @@ namespace MHServerEmu.DatabaseAccess.Json
return false;
}
public bool TryGetLastLogoutTime(ulong playerDbId, out long lastLogoutTime)
{
lastLogoutTime = 0;
return false;
}
public bool InsertAccount(DBAccount account)
{
return Logger.WarnReturn(false, "InsertAccount(): Operation not supported");
@ -117,6 +124,37 @@ namespace MHServerEmu.DatabaseAccess.Json
return true;
}
#region Guilds
// TODO: Guilds are currently not supported by the JSON backend.
public bool LoadGuilds(List<DBGuild> guilds)
{
return true;
}
public bool SaveGuild(DBGuild guild)
{
return true;
}
public bool DeleteGuild(DBGuild guild)
{
return true;
}
public bool SaveGuildMember(DBGuildMember guildMember)
{
return true;
}
public bool DeleteGuildMember(DBGuildMember guildMember)
{
return true;
}
#endregion
/// <summary>
/// Creates a backup of the account file if enough time has passed since the last one.
/// </summary>

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
</PropertyGroup>
@ -20,6 +20,8 @@
<None Remove="SQLite\Scripts\Migrations\1.sql" />
<None Remove="SQLite\Scripts\Migrations\2.sql" />
<None Remove="SQLite\Scripts\Migrations\3.sql" />
<None Remove="SQLite\Scripts\Migrations\4.sql" />
<None Remove="SQLite\Scripts\Migrations\5.sql" />
</ItemGroup>
<ItemGroup>
@ -29,6 +31,8 @@
<EmbeddedResource Include="SQLite\Scripts\Migrations\1.sql" />
<EmbeddedResource Include="SQLite\Scripts\Migrations\2.sql" />
<EmbeddedResource Include="SQLite\Scripts\Migrations\3.sql" />
<EmbeddedResource Include="SQLite\Scripts\Migrations\4.sql" />
<EmbeddedResource Include="SQLite\Scripts\Migrations\5.sql" />
</ItemGroup>
<ItemGroup>

View file

@ -16,12 +16,13 @@ namespace MHServerEmu.DatabaseAccess.Models
[Flags]
public enum AccountFlags
{
None = 0,
IsBanned = 1 << 0,
IsArchived = 1 << 1,
IsPasswordExpired = 1 << 2,
DEPRECATEDLinuxCompatibilityMode = 1 << 3, // This flag used to disable session token verification, but it is no longer needed for Linux users
IsWhitelisted = 1 << 4,
None = 0,
IsBanned = 1 << 0,
IsArchived = 1 << 1,
IsPasswordExpired = 1 << 2,
// 1 << 3 was previously used in 0.x for the Linux compatibility mode, it should not be set in any 1.x+ databases.
IsWhitelisted = 1 << 4,
BypassLoginQueue = 1 << 5,
}
/// <summary>
@ -29,8 +30,12 @@ namespace MHServerEmu.DatabaseAccess.Models
/// </summary>
public class DBAccount
{
private const int LockTimeoutMS = 3000;
private static readonly IdGenerator IdGenerator = new(IdType.Player, 0);
private readonly SemaphoreSlim _semaphore = new(1, 1);
public long Id { get; set; }
public string Email { get; set; }
public string PlayerName { get; set; }
@ -47,6 +52,10 @@ namespace MHServerEmu.DatabaseAccess.Models
public DBEntityCollection Items { get; init; } = new();
public DBEntityCollection ControlledEntities { get; init; } = new();
// Imitate ReplicateForTransfer behavior by having a DBEntityCollection that doesn't get saved to the database.
[JsonIgnore]
public DBEntityCollection TransferredEntities { get; } = new();
// MigrationData is explicitly not saved and exists only as long as the current session does
[JsonIgnore]
public MigrationData MigrationData { get; } = new();
@ -88,12 +97,66 @@ namespace MHServerEmu.DatabaseAccess.Models
return $"{PlayerName} (0x{Id:X})";
}
public LockScope Lock()
{
bool lockTaken = _semaphore.Wait(LockTimeoutMS);
return new(this, lockTaken);
}
public void Unlock()
{
_semaphore.Release();
}
public void ClearEntities()
{
Avatars.Clear();
TeamUps.Clear();
Items.Clear();
ControlledEntities.Clear();
TransferredEntities.Clear();
}
public EntityUpdateScope BeginEntityUpdate()
{
Avatars.BeginUpdate();
TeamUps.BeginUpdate();
Items.BeginUpdate();
ControlledEntities.BeginUpdate();
TransferredEntities.BeginUpdate();
return new(this);
}
public void EndEntityUpdate()
{
Avatars.EndUpdate();
TeamUps.EndUpdate();
Items.EndUpdate();
ControlledEntities.EndUpdate();
TransferredEntities.EndUpdate();
}
public readonly struct LockScope(DBAccount account, bool lockTaken) : IDisposable
{
public readonly DBAccount Account = account;
public readonly bool LockTaken = lockTaken;
public void Dispose()
{
if (LockTaken)
Account.Unlock();
}
}
public readonly struct EntityUpdateScope(DBAccount account) : IDisposable
{
public readonly DBAccount Account = account;
public void Dispose()
{
Account.EndEntityUpdate();
}
}
}
}

View file

@ -16,12 +16,15 @@ namespace MHServerEmu.DatabaseAccess.Models
/// </summary>
public class DBEntityCollection
{
// TODO: Calculate checksum for added entities and update only those that changed
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly Dictionary<long, DBEntity> _allEntities = new(); // All DBEntity instances stored in this collection
private readonly Dictionary<long, List<DBEntity>> _bucketedEntities = new(); // Stored DBEntity bucketed per container
private Dictionary<long, DBEntity> _allEntities = new(); // All DBEntity instances stored in this collection
private Dictionary<long, DBEntity> _dirtyEntities = new();
private bool _isUpdating;
public IEnumerable<DBEntity> Entries { get => _allEntities.Values; }
public int Count { get => _allEntities.Count; }
@ -77,6 +80,59 @@ namespace MHServerEmu.DatabaseAccess.Models
bucket.Clear();
}
public void BeginUpdate()
{
if (_isUpdating)
throw new InvalidOperationException("Entity update is already in progress.");
// Do not remove entities yet, we will reuse them if they are added back.
(_allEntities, _dirtyEntities) = (_dirtyEntities, _allEntities);
foreach (List<DBEntity> bucket in _bucketedEntities.Values)
bucket.Clear();
_isUpdating = true;
}
public void EndUpdate()
{
if (_isUpdating == false)
throw new InvalidOperationException("Entity update is not in progress.");
_dirtyEntities.Clear();
_isUpdating = false;
}
public bool UpdateEntity(long dbGuid, long containerDbGuid, long inventoryProtoGuid, uint slot, long entityProtoGuid, Span<byte> archiveData)
{
if (_isUpdating == false)
throw new InvalidOperationException("Entity update is not in progress.");
// Reuse existing DBEntity instances if possible.
if (_dirtyEntities.Remove(dbGuid, out DBEntity dbEntity) == false)
dbEntity = new();
dbEntity.DbGuid = dbGuid;
dbEntity.ContainerDbGuid = containerDbGuid;
dbEntity.InventoryProtoGuid = inventoryProtoGuid;
dbEntity.Slot = slot;
dbEntity.EntityProtoGuid = entityProtoGuid;
// Do not allocate new archive data buffers if we can reuse the ones we already have.
Span<byte> oldArchiveData = dbEntity.ArchiveData ?? Span<byte>.Empty;
if (archiveData.SequenceEqual(oldArchiveData) == false)
{
// Overwrite existing buffer if the size matches.
if (archiveData.Length == oldArchiveData.Length)
archiveData.CopyTo(oldArchiveData);
else
dbEntity.ArchiveData = archiveData.ToArray();
}
return Add(dbEntity);
}
public bool Contains(long dbGuid)
{
return _allEntities.ContainsKey(dbGuid);

View file

@ -0,0 +1,29 @@
namespace MHServerEmu.DatabaseAccess.Models
{
public class DBGuild
{
public long Id { get; set; }
public string Name { get; set; }
public string Motd { get; set; }
public long CreatorDbGuid { get; set; }
public long CreationTime { get; set; }
// CreatorDbGuid and CreationTime are just additional metadata for tracking/moderation.
public List<DBGuildMember> Members { get; init; } = new();
public DBGuild(long id, string name, string motd, long creatorDbGuid, long creationTime)
{
Id = id;
Name = name;
Motd = motd;
CreatorDbGuid = creatorDbGuid;
CreationTime = creationTime;
}
public override string ToString()
{
return $"{Name} ({Id})";
}
}
}

View file

@ -0,0 +1,21 @@
namespace MHServerEmu.DatabaseAccess.Models
{
public class DBGuildMember
{
public long PlayerDbGuid { get; set; }
public long GuildId { get; set; }
public long Membership { get; set; } // This needs to be long for our Dapper/System.Data.SQLite combo.
public DBGuildMember(long playerDbGuid, long guildId, long membership)
{
PlayerDbGuid = playerDbGuid;
GuildId = guildId;
Membership = membership;
}
public override string ToString()
{
return $"guildId={GuildId}, playerDbGuid=0x{PlayerDbGuid:X}, membership={Membership}";
}
}
}

View file

@ -4,26 +4,48 @@ namespace MHServerEmu.DatabaseAccess.Models
{
public class MigrationData
{
// Store everything here as ulong, PropertyCollection will sort it out game-side
private readonly Dictionary<ulong, List<(ulong, ulong)>> _properties = new(32);
public bool IsInErrorState { get; set; }
public bool SkipNextUpdate { get; set; }
public bool IsFirstLoad { get; set; } = true;
// Store everything here as ulong, PropertyCollection will sort it out game-side
public List<KeyValuePair<ulong, ulong>> PlayerProperties { get; } = new(256);
public List<(ulong, ulong)> WorldView { get; } = new();
public byte[] MatchQueueStatus { get; set; }
public List<CommunityMemberBroadcast> CommunityStatus { get; } = new();
// TODO: Summoned inventory
public MigrationData() { }
public List<(ulong, ulong)> GetOrCreatePropertyList(ulong entityDbId)
{
if (_properties.TryGetValue(entityDbId, out List<(ulong, ulong)> list) == false)
{
list = new();
_properties.Add(entityDbId, list);
}
return list;
}
public void RemovePropertyList(ulong entityDbId)
{
_properties.Remove(entityDbId);
}
public void Reset()
{
IsInErrorState = false;
SkipNextUpdate = false;
IsFirstLoad = true;
PlayerProperties.Clear();
// Properties for summoned entities need to be migrated, and these have arbitrary runtime dbIds, so just clear everything.
_properties.Clear();
WorldView.Clear();
MatchQueueStatus = null;
CommunityStatus.Clear();
}
}

View file

@ -13,7 +13,8 @@ namespace MHServerEmu.DatabaseAccess.SQLite
/// </summary>
public class SQLiteDBManager : IDBManager
{
private const int CurrentSchemaVersion = 4; // Increment this when making changes to the database schema
private const int CurrentSchemaVersion = 6; // Increment this when making changes to the database schema
private const int MinimumSchemaVersion = 6; // Used to ignore legacy 0.x database files.
private const int NumTestAccounts = 5; // Number of test accounts to create for new database files
private const int NumPlayerDataWriteAttempts = 3; // Number of write attempts to do when saving player data
@ -37,7 +38,10 @@ namespace MHServerEmu.DatabaseAccess.SQLite
var config = ConfigManager.Instance.GetConfig<SQLiteDBManagerConfig>();
_dbFilePath = Path.Combine(FileHelper.DataDirectory, config.FileName);
_connectionString = $"Data Source={_dbFilePath};Synchronous=NORMAL;";
_connectionString = $"Data Source={_dbFilePath};Synchronous=NORMAL;foreign_keys=OFF;";
// TODO: Foreign key constraints are explicitly disabled for now because our Item table references
// multiple parent tables (Player / Avatar / TeamUp) at the same time. Need to find an elegant way to fix that.
if (File.Exists(_dbFilePath) == false)
{
@ -62,10 +66,10 @@ namespace MHServerEmu.DatabaseAccess.SQLite
public bool TryQueryAccountByEmail(string email, out DBAccount account)
{
using SQLiteConnection connection = GetConnection();
var accounts = connection.Query<DBAccount>("SELECT * FROM Account WHERE Email = @Email", new { Email = email });
// Associated player data is loaded separately
account = accounts.FirstOrDefault();
// This is just the base account entry, associated player data is loaded separately
account = connection.QueryFirstOrDefault<DBAccount>("SELECT * FROM Account WHERE Email = @Email COLLATE NOCASE", new { Email = email });
return account != null;
}
@ -90,11 +94,11 @@ namespace MHServerEmu.DatabaseAccess.SQLite
return true;
}
public bool TryGetPlayerName(ulong id, out string playerName)
public bool TryGetPlayerName(ulong playerDbId, out string playerName)
{
using SQLiteConnection connection = GetConnection();
playerName = connection.QueryFirstOrDefault<string>("SELECT PlayerName FROM Account WHERE Id = @Id", new { Id = (long)id });
playerName = connection.QueryFirstOrDefault<string>("SELECT PlayerName FROM Account WHERE Id = @Id", new { Id = (long)playerDbId });
return string.IsNullOrWhiteSpace(playerName) == false;
}
@ -111,6 +115,15 @@ namespace MHServerEmu.DatabaseAccess.SQLite
return playerNames.Count > 0;
}
public bool TryGetLastLogoutTime(ulong playerDbId, out long lastLogoutTime)
{
using SQLiteConnection connection = GetConnection();
lastLogoutTime = connection.QueryFirstOrDefault<long>("SELECT LastLogoutTime FROM Player WHERE DbGuid = @DbGuid", new { DbGuid = (long)playerDbId });
return lastLogoutTime > 0;
}
public bool InsertAccount(DBAccount account)
{
lock (_writeLock)
@ -204,6 +217,129 @@ namespace MHServerEmu.DatabaseAccess.SQLite
return Logger.WarnReturn(false, $"SavePlayerData(): Failed to write player data for account [{account}]");
}
public bool LoadGuilds(List<DBGuild> outGuilds)
{
try
{
using SQLiteConnection connection = GetConnection();
IEnumerable<DBGuild> guildQueryResult = connection.Query<DBGuild>("SELECT * FROM Guild");
IEnumerable<DBGuildMember> memberQueryResult = connection.Query<DBGuildMember>("SELECT * FROM GuildMember");
outGuilds.AddRange(guildQueryResult);
// This is going to be called only on server startup, so it's fine not to pool this.
Dictionary<long, DBGuild> guildLookup = new(outGuilds.Count);
foreach (DBGuild guild in outGuilds)
guildLookup.Add(guild.Id, guild);
foreach (DBGuildMember member in memberQueryResult)
{
if (guildLookup.TryGetValue(member.GuildId, out DBGuild guild) == false)
{
Logger.Warn($"LoadGuilds(): Found orphan member [{member}]");
continue;
}
guild.Members.Add(member);
}
return true;
}
catch (Exception e)
{
outGuilds.Clear();
Logger.ErrorException(e, nameof(LoadGuilds));
return false;
}
}
public bool SaveGuild(DBGuild guild)
{
try
{
using SQLiteConnection connection = GetConnection();
int inserted = connection.Execute("INSERT OR IGNORE INTO Guild (Id, Name, Motd, CreatorDbGuid, CreationTime) VALUES (@Id, @Name, @Motd, @CreatorDbGuid, @CreationTime)", guild);
// Only name and MOTD should be mutable after creation.
if (inserted == 0)
connection.Execute("UPDATE Guild SET Name=@Name, Motd=@Motd WHERE Id=@Id", guild);
Logger.Trace($"SaveGuild(): {guild}");
return true;
}
catch (Exception e)
{
Logger.ErrorException(e, nameof(SaveGuild));
return false;
}
}
public bool DeleteGuild(DBGuild guild)
{
using SQLiteConnection connection = GetConnection();
using SQLiteTransaction transaction = connection.BeginTransaction();
try
{
// TODO: Enable foreign key constraints in the connection string and just delete the row from the parent table when we fix the Item table.
connection.Execute("DELETE FROM GuildMember WHERE GuildId = @Id", guild, transaction);
connection.Execute("DELETE FROM Guild WHERE Id = @Id", guild, transaction);
transaction.Commit();
Logger.Trace($"DeleteGuild(): {guild}");
return true;
}
catch (Exception e)
{
transaction.Rollback();
Logger.ErrorException(e, nameof(DeleteGuild));
return false;
}
}
public bool SaveGuildMember(DBGuildMember guildMember)
{
try
{
using SQLiteConnection connection = GetConnection();
int inserted = connection.Execute("INSERT OR IGNORE INTO GuildMember (PlayerDbGuid, GuildId, Membership) VALUES (@PlayerDbGuid, @GuildId, @Membership)", guildMember);
// Only membership should be mutable after creation.
if (inserted == 0)
connection.Execute("UPDATE GuildMember SET Membership=@Membership WHERE PlayerDbGuid=@PlayerDbGuid", guildMember);
Logger.Trace($"SaveGuildMember(): {guildMember}");
return true;
}
catch (Exception e)
{
Logger.ErrorException(e, nameof(SaveGuildMember));
return false;
}
}
public bool DeleteGuildMember(DBGuildMember guildMember)
{
try
{
using SQLiteConnection connection = GetConnection();
connection.Execute("DELETE FROM GuildMember WHERE PlayerDbGuid = @PlayerDbGuid", guildMember);
Logger.Trace($"DeleteGuildMember(): {guildMember}");
return true;
}
catch (Exception e)
{
Logger.ErrorException(e, nameof(DeleteGuildMember));
return false;
}
}
/// <summary>
/// Creates and opens a new <see cref="SQLiteConnection"/>.
/// </summary>
@ -262,6 +398,18 @@ namespace MHServerEmu.DatabaseAccess.SQLite
if (schemaVersion > CurrentSchemaVersion)
return Logger.ErrorReturn(false, $"Initialize(): Existing database file uses unsupported schema version {schemaVersion} (current = {CurrentSchemaVersion})");
if (schemaVersion < MinimumSchemaVersion)
{
Logger.Warn($"Found existing database file with legacy schema version {schemaVersion}, which is not supported by this version of MHServerEmu");
// Need to dispose the connection before moving the database file so that the file is not in use.
connection.Dispose();
File.Move(_dbFilePath, $"{_dbFilePath}.old");
InitializeDatabaseFile();
return true;
}
Logger.Info($"Found existing database file with schema version {schemaVersion} (current = {CurrentSchemaVersion})");
if (schemaVersion == CurrentSchemaVersion)
@ -323,9 +471,9 @@ namespace MHServerEmu.DatabaseAccess.SQLite
// Update player entity
if (account.Player != null)
{
connection.Execute(@$"INSERT OR IGNORE INTO Player (DbGuid) VALUES (@DbGuid)", account.Player, transaction);
connection.Execute(@$"UPDATE Player SET ArchiveData=@ArchiveData, StartTarget=@StartTarget,
AOIVolume=@AOIVolume, GazillioniteBalance=@GazillioniteBalance WHERE DbGuid = @DbGuid",
connection.Execute(@"INSERT OR IGNORE INTO Player (DbGuid) VALUES (@DbGuid)", account.Player, transaction);
connection.Execute(@"UPDATE Player SET ArchiveData=@ArchiveData, StartTarget=@StartTarget, AOIVolume=@AOIVolume,
GazillioniteBalance=@GazillioniteBalance, LastLogoutTime=@LastLogoutTime WHERE DbGuid = @DbGuid",
account.Player, transaction);
}
else

View file

@ -67,24 +67,19 @@ namespace MHServerEmu.DatabaseAccess.SQLite
public void UpdateEntities(SQLiteConnection connection, SQLiteTransaction transaction, long containerDbGuid, DBEntityCollection dbEntityCollection)
{
// Delete items that no longer belong to this account
List<long> entitiesToDelete = ListPool<long>.Instance.Get();
using var entitiesToDeleteHandle = ListPool<long>.Instance.Get(out List<long> entitiesToDelete);
GetEntitiesToDelete(connection, containerDbGuid, dbEntityCollection, entitiesToDelete);
try
{
if (entitiesToDelete.Count > 0)
connection.Execute(_deleteQuery, new { EntitiesToDelete = entitiesToDelete });
}
finally
{
// Make sure the list is returned to the pool even if the deletion query fails.
ListPool<long>.Instance.Return(entitiesToDelete);
}
if (entitiesToDelete.Count > 0)
connection.Execute(_deleteQuery, new { EntitiesToDelete = entitiesToDelete });
// Insert and update
IReadOnlyList<DBEntity> entries = dbEntityCollection.GetEntriesForContainer(containerDbGuid);
connection.Execute(_insertQuery, entries, transaction);
connection.Execute(_updateQuery, entries, transaction);
if (entries.Count > 0)
{
connection.Execute(_insertQuery, entries, transaction);
connection.Execute(_updateQuery, entries, transaction);
}
}
/// <summary>

View file

@ -1,6 +1,6 @@
-- Initialize a new database file using the current schema version
PRAGMA user_version=4;
PRAGMA user_version=6;
PRAGMA journal_mode=WAL;
CREATE TABLE "Account" (
@ -20,6 +20,7 @@ CREATE TABLE "Player" (
"StartTarget" INTEGER,
"AOIVolume" INTEGER,
"GazillioniteBalance" INTEGER,
"LastLogoutTime" INTEGER,
FOREIGN KEY("DbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
PRIMARY KEY("DbGuid")
);
@ -70,6 +71,24 @@ CREATE TABLE "ControlledEntity" (
PRIMARY KEY("DbGuid")
);
CREATE TABLE "Guild" (
"Id" INTEGER NOT NULL UNIQUE,
"Name" TEXT NOT NULL UNIQUE,
"Motd" TEXT NOT NULL,
"CreatorDbGuid" INTEGER,
"CreationTime" INTEGER,
PRIMARY KEY("Id")
);
CREATE TABLE "GuildMember" (
"PlayerDbGuid" INTEGER NOT NULL UNIQUE,
"GuildId" INTEGER NOT NULL,
"Membership" INTEGER NOT NULL,
FOREIGN KEY("PlayerDbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
FOREIGN KEY("GuildId") REFERENCES "Guild"("Id") ON DELETE CASCADE,
PRIMARY KEY("PlayerDbGuid")
);
CREATE INDEX "IX_Avatar_ContainerDbGuid" ON "Avatar" ("ContainerDbGuid");
CREATE INDEX "IX_TeamUp_ContainerDbGuid" ON "TeamUp" ("ContainerDbGuid");
CREATE INDEX "IX_Item_ContainerDbGuid" ON "Item" ("ContainerDbGuid");

View file

@ -0,0 +1,26 @@
-- Add guild data.
-- Add last logout time to players and initialize it to 0.
ALTER TABLE Player ADD COLUMN LastLogoutTime INTEGER;
UPDATE Player SET LastLogoutTime=0;
-- Create guild tables.
CREATE TABLE "Guild" (
"Id" INTEGER NOT NULL UNIQUE,
"Name" TEXT NOT NULL UNIQUE,
"Motd" TEXT NOT NULL,
"CreatorDbGuid" INTEGER,
"CreationTime" INTEGER,
PRIMARY KEY("Id")
);
CREATE TABLE "GuildMember" (
"PlayerDbGuid" INTEGER NOT NULL UNIQUE,
"GuildId" INTEGER NOT NULL,
"Membership" INTEGER NOT NULL,
FOREIGN KEY("PlayerDbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
FOREIGN KEY("GuildId") REFERENCES "Guild"("Id") ON DELETE CASCADE,
PRIMARY KEY("PlayerDbGuid")
);
-- We do not need indexing for these tables because we are going to load all guilds into memory.

View file

@ -0,0 +1 @@
-- Empty version, this is used only to prevent 0.x database files from being loaded in 1.x+.

View file

@ -3,7 +3,7 @@ using Google.ProtocolBuffers;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Network;
using MHServerEmu.Core.Network.Tcp;
using MHServerEmu.Core.System;
using MHServerEmu.Core.RateLimiting;
using MHServerEmu.DatabaseAccess;
using MHServerEmu.DatabaseAccess.Models;
@ -14,14 +14,13 @@ namespace MHServerEmu.Frontend
/// </summary>
public class FrontendClient : TcpClient, IFrontendClient, IDBAccountOwner
{
// We are currently allowing 50 packets per seconds with up to 10 seconds of burst.
// Given our current receive buffer size of 8 KB, this limits client input at about 400 KB/s.
private const int RateLimitPacketsPerSecond = 50;
private const int RateLimitBurst = RateLimitPacketsPerSecond * 10;
// Rate limit at 8 KB/s with a bit of burst allowed.
private const int RateLimitBytesPerSecond = 1024 * 8;
private const int RateLimitBurst = RateLimitBytesPerSecond * 10;
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly TokenBucket _tokenBucket = new(RateLimitPacketsPerSecond, RateLimitBurst);
private readonly TokenBucket _tokenBucket = new(RateLimitBytesPerSecond, RateLimitBurst);
private readonly MuxReader _muxReader;
// We intentionally don't use an array here so that channel state is inlined in FrontendClient
@ -138,7 +137,7 @@ namespace MHServerEmu.Frontend
/// </summary>
public void HandleIncomingData(byte[] buffer, int length)
{
if (_tokenBucket.CheckLimit() == false)
if (_tokenBucket.CheckLimit(length) == false)
{
Logger.Error($"HandleIncomingData(): Rate limit exceeded for client [{this}]");
Disconnect();
@ -241,10 +240,10 @@ namespace MHServerEmu.Frontend
var clientCredentials = messageBuffer.Deserialize<FrontendProtocolMessage>() as ClientCredentials;
if (clientCredentials == null) return Logger.ErrorReturn(false, $"OnClientCredentials(): Failed to retrieve message");
// Routing this message should authenticate the client if the credentials are successfully verified
MailboxMessage mailboxMessage = new(messageBuffer.MessageId, clientCredentials);
ServiceMessage.RouteMessage routeMessage = new(_client, typeof(FrontendProtocolMessage), mailboxMessage);
ServerManager.Instance.SendMessageToService(GameServiceType.PlayerManager, routeMessage);
// Routing this message should authenticate the client if the credentials are successfully verified.
// If we ever split the frontend into a separate process we will need to replicate session assignment between the processes.
ServiceMessage.SessionVerificationRequest sessionVerificationRequest = new(_client, clientCredentials);
ServerManager.Instance.SendMessageToService(GameServiceType.PlayerManager, sessionVerificationRequest);
return true;
}

View file

@ -52,9 +52,10 @@ namespace MHServerEmu.Frontend
}
}
public string GetStatus()
public void GetStatus(Dictionary<string, long> statusDict)
{
return $"Connections: {ConnectionCount} | Clients: {_clients.Count}";
statusDict["FrontendConnections"] = ConnectionCount;
statusDict["FrontendClients"] = _clients.Count;
}
#endregion

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
</PropertyGroup>

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