Compare commits

..

No commits in common. "master" and "0.7.0" have entirely different histories.

487 changed files with 80295 additions and 165597 deletions

View file

@ -1,29 +0,0 @@
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,6 +402,3 @@ FodyWeavers.xsd
.*#
.#*
# Build directory
/build

View file

@ -1,29 +0,0 @@
@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

9
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,9 @@
# 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
## Developers
## Contributors
- AlexBond
@ -8,34 +8,18 @@
- 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 Gazillion developers who worked on the game in any capacity.
- All the people who worked on the game in any capacity.

View file

@ -17,7 +17,9 @@ 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.WebFrontend", "src\MHServerEmu.WebFrontend\MHServerEmu.WebFrontend.csproj", "{4302339A-9C4B-47A9-8C26-F0DC66EA08F7}"
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}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MHServerEmu.Leaderboards", "src\MHServerEmu.Leaderboards\MHServerEmu.Leaderboards.csproj", "{2C74480B-B373-4DFD-9C9C-E652E889E9C1}"
EndProject
@ -65,6 +67,10 @@ 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,6 +6,8 @@ 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.
@ -30,13 +32,25 @@ 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
**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?**
**Where can I download the game?**
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.
@ -44,11 +58,13 @@ 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, 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.
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.
**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,50 +94,44 @@ 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,7 +17,6 @@ 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 `2026.03.25 14:52:06 UTC` using server version `1.0.0`.
This list was automatically generated on `2025.08.16 18:28:24 UTC` using server version `0.7.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. | Moderator | Client |
| !account info | Shows information for the logged in account. | Any | 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,10 +24,9 @@ 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 |
| !achievement localeid | Generates a LocaleStringId from the specified argument. | Admin | Any |
| Command | Description | User Level | Invoker Type |
| ---------------------- | ------------------------------------------- | ---------- | ------------ |
| !achievement info [id] | Outputs info for the specified achievement. | Admin | Any |
## AOI
Commands for interacting with the invoker player's area of interest (AOI).
@ -45,8 +44,6 @@ 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
@ -108,7 +105,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 credits to a sellable chest item. | Any | Client |
| !item creditchest | Converts 500k 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 |
@ -211,33 +208,20 @@ Region management commands.
## Server
Server management commands.
| 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 |
| 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 |
## 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 [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 |
| 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 |
## Unlock
Commands for unlocking various things.
@ -247,14 +231,6 @@ 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 |
@ -265,7 +241,6 @@ Web API management commands.
| !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 |

View file

@ -1,239 +0,0 @@
# 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,30 +41,16 @@ 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 [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 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 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/Dashboard/](http://localhost:8080/Dashboard/) 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/AccountManagement/Create 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/Dashboard/](http://localhost:8080/Dashboard/). 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/AccountManagement/Create](http://localhost:8080/AccountManagement/Create). 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,19 +1,6 @@
# Embedded Browser
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 |
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.
## API Calls
@ -63,11 +50,3 @@ 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

@ -0,0 +1,11 @@
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

@ -0,0 +1,159 @@
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

@ -0,0 +1,73 @@
<!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

@ -0,0 +1,58 @@
<!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

@ -0,0 +1,96 @@
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

@ -0,0 +1,213 @@
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

@ -0,0 +1,57 @@
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

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
</PropertyGroup>
@ -25,19 +25,10 @@
</ItemGroup>
<ItemGroup>
<None Update="Data\Web\Dashboard\config.js">
<None Update="Data\Auth\AccountCreateForm.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Data\Web\Dashboard\index.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Data\Web\Dashboard\script.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Data\Web\Dashboard\style.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Data\Web\MTXStore\add-g.html">
<None Update="Data\Auth\Response.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

View file

@ -0,0 +1,46 @@
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

@ -0,0 +1,26 @@
<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

@ -1,12 +1,8 @@
namespace MHServerEmu.Core.Collections
{
public readonly struct FixedPriorityQueue<T> where T : IComparable<T>
public class 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];
@ -16,11 +12,6 @@
_items = new(capacity);
}
public FixedPriorityQueue(List<T> items)
{
_items = items;
}
public void Push(T value)
{
_items.Add(value);
@ -36,10 +27,7 @@
_items.RemoveAt(_items.Count - 1);
}
public void Clear()
{
_items.Clear();
}
public void Clear() => _items.Clear();
public void Heapify()
{
@ -116,4 +104,102 @@
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

@ -1,100 +0,0 @@
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,49 +0,0 @@
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,59 +1,58 @@
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; private set; }
public T Head { get; set; }
public T Tail { get; private set; }
public int Count { get; private set; }
public bool IsEmpty { get => Head == null; }
private Iterator[] _iterators;
private int _numIterators;
private int _maxIterators;
public InvasiveList(int maxIterators, int id = 0)
public InvasiveList(int maxIterators)
{
_iterators = new Iterator[maxIterators];
if (maxIterators > 1)
_iteratorPool = new();
_maxIterators = maxIterators;
_iterators = new Iterator[_maxIterators];
}
public InvasiveList(int maxIterators, int id)
{
_maxIterators = maxIterators;
_iterators = new Iterator[_maxIterators];
Id = id;
}
public IEnumerator<T> GetEnumerator()
public IEnumerable<T> Iterate()
{
Iterator iterator;
var iterator = new Iterator(this);
if (_iteratorPool != null)
try
{
if (_iteratorPool.TryPop(out iterator) == false)
iterator = new(this);
while (iterator.End() == false)
{
var element = iterator.Current;
iterator.MoveNext();
yield return element;
}
}
else
finally
{
_reusableIterator ??= new(this);
iterator = _reusableIterator;
UnregisterIterator(iterator);
}
iterator.Initialize();
return iterator;
}
public bool IsEmpty() => Head == null;
public void Remove(T element)
{
if (element == null || Contains(element) == false) return;
ref var node = ref GetInvasiveListNode(element, Id);
if (Unsafe.IsNullRef(ref node)) return;
var node = GetInvasiveListNode(element, Id);
if (node == null) return;
for (int i = 0; i < _numIterators; i++)
{
@ -69,16 +68,16 @@ namespace MHServerEmu.Core.Collections
if (node.Next != null)
{
T nextElement = node.Next;
ref var nextNode = ref GetInvasiveListNode(nextElement, Id);
if (Unsafe.IsNullRef(ref nextNode) == false)
var nextNode = GetInvasiveListNode(nextElement, Id);
if (nextNode != null)
nextNode.Prev = node.Prev;
}
if (node.Prev != null)
{
T prevElement = node.Prev;
ref var prevNode = ref GetInvasiveListNode(prevElement, Id);
if (Unsafe.IsNullRef(ref prevNode) == false)
var prevNode = GetInvasiveListNode(prevElement, Id);
if (prevNode != null)
prevNode.Next = node.Next;
}
@ -94,11 +93,11 @@ namespace MHServerEmu.Core.Collections
if (oldElement == null || Contains(oldElement) == false) return;
if (element == null || Contains(element)) return;
ref var node = ref GetInvasiveListNode(element, Id);
if (Unsafe.IsNullRef(ref node)) return;
var node = GetInvasiveListNode(element, Id);
if (node == null) return;
ref var oldNode = ref GetInvasiveListNode(oldElement, Id);
if (Unsafe.IsNullRef(ref oldNode)) return;
var oldNode = GetInvasiveListNode(oldElement, Id);
if (oldNode == null) return;
var oldPrev = oldNode.Prev;
oldNode.Prev = element;
@ -107,8 +106,8 @@ namespace MHServerEmu.Core.Collections
if (oldPrev != null)
{
ref var oldPrevNode = ref GetInvasiveListNode(oldPrev, Id);
if (Unsafe.IsNullRef(ref oldPrevNode)) return;
var oldPrevNode = GetInvasiveListNode(oldPrev, Id);
if (oldPrevNode == null) return;
oldPrevNode.Next = element;
}
else
@ -121,14 +120,14 @@ namespace MHServerEmu.Core.Collections
{
if (element == null || Contains(element)) return;
ref var node = ref GetInvasiveListNode(element, Id);
if (Unsafe.IsNullRef(ref node)) return;
var node = GetInvasiveListNode(element, Id);
if (node == null) return;
node.Prev = Tail;
if (Tail != null)
{
ref var tailNode = ref GetInvasiveListNode(Tail, Id);
if (Unsafe.IsNullRef(ref tailNode)) return;
var tailNode = GetInvasiveListNode(Tail, Id);
if (tailNode == null) return;
tailNode.Next = element;
}
else
@ -138,23 +137,20 @@ namespace MHServerEmu.Core.Collections
Count++;
}
public virtual ref InvasiveListNode<T> GetInvasiveListNode(T element, int listId)
{
return ref Unsafe.NullRef<InvasiveListNode<T>>();
}
public virtual InvasiveListNode<T> GetInvasiveListNode(T element, int listId) => null;
public bool Contains(T element)
{
if (element == null) return false;
ref var node = ref GetInvasiveListNode(element, Id);
if (Unsafe.IsNullRef(ref node)) return false;
var node = GetInvasiveListNode(element, Id);
if (node == null) return false;
return node.Next != null || node.Prev != null || element.Equals(Head);
}
private void RegisterIterator(Iterator iterator)
{
if (_numIterators >= _iterators.Length)
throw new InvalidOperationException($"Too many iterators '{_iterators.Length}' for invasive list");
if (_numIterators >= _maxIterators)
throw new InvalidOperationException($"Too many iterators '{_maxIterators}' for invasive list");
_iterators[_numIterators++] = iterator;
}
@ -169,75 +165,71 @@ 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 sealed class Iterator : IEnumerator<T>
public class Iterator : IEnumerator<T>
{
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;
private InvasiveList<T> _list;
public bool SkipNext { get; set; }
public Iterator(InvasiveList<T> invasiveList)
{
_list = invasiveList;
}
public void Initialize()
{
Current = _list.Head;
SkipNext = false;
_list.RegisterIterator(this);
}
public void Dispose()
{
_list.UnregisterIterator(this);
}
public void Reset()
{
_start = true;
Current = default;
SkipNext = false;
}
public T Current { get; private set; }
object IEnumerator.Current => Current;
public void Dispose() { }
public void Reset() { }
public bool MoveNext()
{
if (_start)
{
Current = _list.Head;
_start = false;
}
else
{
if (SkipNext)
SkipNext = false;
else if (Current != null)
Current = _list.GetInvasiveListNode(Current, _list.Id).Next;
}
if (SkipNext) SkipNext = false;
else if (Current != null)
Current = _list.GetInvasiveListNode(Current, _list.Id).Next;
return Current != null;
return true;
}
public bool End() => Current == null;
}
}
public struct InvasiveListNode<T>
public class InvasiveListNode<T>
{
public T Next;
public T Prev;
public T Next { get; set; }
public T Prev { get; set; }
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,7 +33,9 @@ namespace MHServerEmu.Core.Collections
public Picker(Picker<T> other)
{
_elements = new(other._elements);
_elements = new(other._elements.Count);
foreach (WeightedElement element in other._elements)
_elements.Add(element);
// 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.
@ -186,10 +188,15 @@ namespace MHServerEmu.Core.Collections
_weights = 0;
}
private readonly struct WeightedElement(T element, int weight)
private class WeightedElement
{
public T Element { get; } = element;
public int Weight { get; } = weight;
public T Element { get; }
public int Weight { get; }
public WeightedElement(T element, int weight)
{
Element = element;
Weight = weight;
}
}
}
}

View file

@ -1,22 +0,0 @@
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,12 +70,15 @@ namespace MHServerEmu.Core.Collisions
/// <summary>
/// Return the coordinates of the corners
/// </summary>
public void GetPoints(Span<Point2> points)
public Point2[] GetPoints()
{
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);
return new Point2[]
{
new (Min.X, Min.Y),
new (Min.X, Max.Y),
new (Max.X, Max.Y),
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 InlineArray3<Vector3> Points;
public 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,17 +106,6 @@ 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

@ -1,44 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,37 +0,0 @@
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

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

View file

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

View file

@ -34,23 +34,15 @@ 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)
{
int numBytes = Encoding.UTF8.GetByteCount(str);
Span<byte> bytes = stackalloc byte[numBytes];
Encoding.UTF8.GetBytes(str, bytes);
return Djb2(bytes);
uint hash = 5381;
for (int i = 0; i < str.Length; i++)
hash = (hash << 5) + hash + ((byte)str[i]);
return hash;
}
/// <summary>

View file

@ -0,0 +1,131 @@
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,4 +1,6 @@
namespace MHServerEmu.Core.Logging
using System.Text;
namespace MHServerEmu.Core.Logging
{
/// <summary>
/// A timestamped log message.
@ -7,6 +9,8 @@
{
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; }
@ -32,45 +36,26 @@
public override string ToString()
{
Span<char> formattedTimestamp = stackalloc char[TimeFormat.Length];
Timestamp.TryFormat(formattedTimestamp, out _, TimeFormat);
return $"[{formattedTimestamp}] [{Level,5}] [{Logger}] {Message}";
return $"[{Timestamp.ToString(TimeFormat)}] [{Level,5}] [{Logger}] {Message}";
}
/// <summary>
/// Returns a string that represents this <see cref="LogMessage"/> with or without a timestamp.
/// </summary>
public string ToString(bool includeTimestamp)
public string ToString(bool includeTimestamps)
{
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)
lock (StringBuilder) // This shouldn't be called from multiple threads unless in synchronous mode
{
Span<char> formattedTimestamp = stackalloc char[TimeFormat.Length];
Timestamp.TryFormat(formattedTimestamp, out _, TimeFormat);
writer.Write('[');
writer.Write(formattedTimestamp);
writer.Write(']');
writer.Write(' ');
if (includeTimestamps)
StringBuilder.Append($"[{Timestamp.ToString(TimeFormat)}] ");
StringBuilder.Append($"[{Level,5}] [{Logger}] {Message}");
string str = StringBuilder.ToString();
StringBuilder.Clear();
return str;
}
// 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,6 +1,5 @@
using MHServerEmu.Core.Config;
using System.Collections.Concurrent;
using System.Globalization;
namespace MHServerEmu.Core.Logging
{
@ -9,8 +8,7 @@ namespace MHServerEmu.Core.Logging
/// </summary>
internal static class LogRouter
{
private static readonly BlockingCollection<LogMessage> LogMessages;
private static readonly Thread LogThread;
private static readonly ConcurrentQueue<LogMessage> MessageQueue;
/// <summary>
/// Initializes <see cref="LogRouter"/>.
@ -19,53 +17,51 @@ namespace MHServerEmu.Core.Logging
{
// Initialize async logging if synchronous mode is not enabled
var config = ConfigManager.Instance.GetConfig<LoggingConfig>();
if (config.SynchronousMode)
return;
LogMessages = new();
LogThread = new(RouteLogMessages)
if (config.SynchronousMode == false)
{
Name = "Logging",
IsBackground = true,
CurrentCulture = CultureInfo.InvariantCulture
};
LogThread.Start();
MessageQueue = new();
Task.Run(async () => await RouteMessagesAsync());
}
}
/// <summary>
/// Add a <see cref="LogMessage"/> instance to be routed.
/// Creates a new <see cref="LogMessage"/> instance from the provided arguments and processes it.
/// </summary>
internal static void AddLogMessage(in LogMessage logMessage)
internal static void AddMessage(LoggingLevel level, string logger, string message, LogChannels channels, LogCategory category)
{
if (LogManager.Enabled == false)
return;
if (LogManager.Enabled == false) return;
if (LogMessages != null)
LogMessages.Add(logMessage); // Add the message to the queue to be routed asynchronously
LogMessage logMessage = new(level, logger, message, channels, category);
if (MessageQueue != null)
MessageQueue.Enqueue(logMessage); // Add the message to the queue to be processed asynchronously
else
RouteLogMessage(logMessage); // Route the message right away if async output is disabled (this is slow and should be used only for testing)
RouteMessage(logMessage); // Process the message right away if async output is disabled (note: 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 RouteLogMessage(in LogMessage logMessage)
private static void RouteMessage(in LogMessage message)
{
foreach (LogTarget target in LogManager.IterateTargets(logMessage))
target.ProcessLogMessage(logMessage);
foreach (LogTarget target in LogManager.IterateTargets(message))
target.ProcessLogMessage(message);
}
/// <summary>
/// Processes enqueued <see cref="LogMessage"/>. This should run on its own thread.
/// Processes enqueued <see cref="LogMessage"/> instances asynchronously.
/// </summary>
private static void RouteLogMessages()
private static async Task RouteMessagesAsync()
{
while (true)
{
LogMessage logMessage = LogMessages.Take();
RouteLogMessage(logMessage);
while (MessageQueue.IsEmpty == false)
{
if (MessageQueue.TryDequeue(out LogMessage message))
RouteMessage(message);
}
await Task.Delay(1);
}
}
}

View file

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

View file

@ -18,7 +18,7 @@
public override void ProcessLogMessage(in LogMessage message)
{
SetForegroundColor(message.Level);
message.WriteTo(Console.Out, IncludeTimestamps, true);
Console.WriteLine(message.ToString(IncludeTimestamps));
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);
_writers[(int)category] = new(fs) { AutoFlush = true };
}
}
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)];
_writers = [new(fs) { AutoFlush = true }];
}
}
@ -51,9 +51,8 @@ namespace MHServerEmu.Core.Logging.Targets
if (_disposed)
return;
StreamWriter writer = _writers[_splitOutput ? (int)message.Category : 0];
message.WriteTo(writer, IncludeTimestamps, true);
writer.Flush();
int index = _splitOutput ? (int)message.Category : 0;
_writers[index].WriteLine(message.ToString(IncludeTimestamps));
}
#region IDisposable Implementation

View file

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

View file

@ -1,5 +1,4 @@
using MHServerEmu.Core.Collections;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Memory
{
@ -10,7 +9,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 use shared pools with locks
// For game threads we want to have dedicated pools, in other cases we'll use shared pools with locks
[ThreadStatic]
public static bool UseThreadLocalStorage;
}
@ -44,16 +43,6 @@ 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>
@ -72,27 +61,7 @@ namespace MHServerEmu.Core.Memory
}
/// <summary>
/// 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.
/// Represents a storage unit of a pool or a particular type.
/// </summary>
private class Node
{
@ -142,7 +111,7 @@ namespace MHServerEmu.Core.Memory
private ListPool() { }
/// <summary>
/// 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.
/// 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.
/// </summary>
public List<T> Get(int capacity)
{
@ -152,19 +121,7 @@ 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 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.
/// 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.
/// </summary>
public List<T> Get(IEnumerable<T> collection)
{
@ -172,17 +129,6 @@ 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>
@ -204,14 +150,4 @@ 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

@ -1,37 +0,0 @@
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

@ -1,56 +0,0 @@
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,4 +1,5 @@
using System.Text;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Metrics.Categories
@ -78,7 +79,7 @@ namespace MHServerEmu.Core.Metrics.Categories
return _trackers[(int)metric].AsReportEntry();
}
public readonly struct Report
public readonly struct Report : IHtmlDataStructure
{
public MetricTracker.ReportEntry UpdateTime { get; }
public MetricTracker.ReportEntry FrameTime { get; }
@ -105,6 +106,21 @@ 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,5 +1,6 @@
using System.Runtime;
using System.Text;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
namespace MHServerEmu.Core.Metrics.Categories
@ -62,7 +63,7 @@ namespace MHServerEmu.Core.Metrics.Categories
}
}
public readonly struct Report
public readonly struct Report : IHtmlDataStructure
{
public long GCIndex { get; }
public long GCCountGen0 { get; }
@ -97,6 +98,19 @@ 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,5 +1,7 @@
using MHServerEmu.Core.Collections;
using System.Text;
using MHServerEmu.Core.Collections;
using MHServerEmu.Core.Extensions;
using MHServerEmu.Core.Helpers;
namespace MHServerEmu.Core.Metrics
{
@ -58,8 +60,9 @@ namespace MHServerEmu.Core.Metrics
/// <summary>
/// A snapshot of the state of a <see cref="MetricTracker"/>.
/// </summary>
public readonly struct ReportEntry
public readonly struct ReportEntry : IHtmlDataStructure
{
public string Name { get; }
public float Average { get; }
public float Median { get; }
public float Last { get; }
@ -68,6 +71,7 @@ namespace MHServerEmu.Core.Metrics
public ReportEntry(MetricTracker tracker)
{
Name = tracker._name;
Average = tracker._buffer.ToAverage();
Median = tracker._buffer.ToMedian();
Last = tracker._last;
@ -79,6 +83,17 @@ 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,6 +4,7 @@
{
PlainText,
Json,
Html,
}
public enum GamePerformanceMetricEnum

View file

@ -76,17 +76,12 @@ 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>();
GetPerformanceReportData(report);
lock (_lock)
report.Initialize(_memoryMetrics, _gamePerformanceMetricsDict.Values);
return report.ToString(format);
}

View file

@ -1,6 +1,6 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using MHServerEmu.Core.Helpers;
using MHServerEmu.Core.Logging;
using MHServerEmu.Core.Memory;
using MHServerEmu.Core.Metrics.Categories;
@ -14,23 +14,21 @@ 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, Dictionary<ulong, GamePerformanceMetrics> gameMetrics)
public void Initialize(MemoryMetrics memoryMetrics, IEnumerable<GamePerformanceMetrics> gameMetrics)
{
Id = (ulong)Clock.UnixTime.TotalSeconds << 32 | ++_currentReportId;
Memory = memoryMetrics.GetReport();
foreach (GamePerformanceMetrics metrics in gameMetrics.Values)
foreach (GamePerformanceMetrics metrics in gameMetrics)
{
Games.Add(metrics.GameId, metrics.GetReport());
}
@ -51,6 +49,9 @@ 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}");
}
@ -84,5 +85,24 @@ 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,22 +29,6 @@ namespace MHServerEmu.Core.Network
RemoveResponse,
}
public enum ChatRoomOperationType
{
Add,
Remove,
}
public enum AccountOperation
{
Create,
SetPlayerName,
SetPassword,
SetUserLevel,
SetFlag,
ClearFlag,
}
#endregion
public static class ServiceMessage
@ -81,7 +65,7 @@ namespace MHServerEmu.Core.Network
public readonly MailboxMessage Message = message;
}
#region Player Manager
#region Game Instances
public readonly struct GameInstanceOp(GameInstanceOpType type, ulong gameId)
: IGameServiceMessage
@ -120,16 +104,6 @@ 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>
@ -256,15 +230,6 @@ 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>
@ -363,9 +328,6 @@ 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
{
@ -413,159 +375,33 @@ 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
/// <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)
public readonly struct GroupingManagerChat(IFrontendClient client, NetMessageChat chat, int prestigeLevel, List<ulong> playerFilter)
: IGameServiceMessage
{
public readonly ulong PlayerDbId = playerDbId;
public readonly IFrontendClient Client = client;
public readonly NetMessageChat Chat = chat;
public readonly int PrestigeLevel = prestigeLevel;
public readonly List<ulong> PlayerFilter = playerFilter;
}
/// <summary>
/// [Game -> GroupingManager] Routes a tell chat message from a game instance.
/// </summary>
public readonly struct GroupingManagerTell(ulong playerDbId, NetMessageTell tell, int prestigeLevel)
public readonly struct GroupingManagerTell(IFrontendClient client, NetMessageTell tell, int prestigeLevel)
: IGameServiceMessage
{
public readonly ulong PlayerDbId = playerDbId;
public readonly IFrontendClient Client = client;
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
@ -694,149 +530,5 @@ 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,6 +7,5 @@
{
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>
/// Adds the status of this <see cref="IGameService"/> to the provided dictionary.
/// Returns a <see cref="string"/> representing the status of this <see cref="IGameService"/>.
/// </summary>
public void GetStatus(Dictionary<string, long> statusDict);
public string GetStatus();
}
}

View file

@ -1,5 +1,4 @@
using Google.ProtocolBuffers;
using MHServerEmu.Core.Serialization;
namespace MHServerEmu.Core.Network
{
@ -44,15 +43,5 @@ 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.Collections;
using System.Buffers;
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,9 +13,7 @@ namespace MHServerEmu.Core.Network
public readonly struct MuxPacket : IPacket
{
private static readonly Logger Logger = LogManager.CreateLogger();
// 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 static readonly ArrayPool<byte> BufferPool = ArrayPool<byte>.Create();
private readonly List<MessagePackageOut> _outboundMessageList = null;
@ -41,16 +39,7 @@ namespace MHServerEmu.Core.Network
Command = command;
if (IsDataPacket)
_outboundMessageList = MessageListPool.Get();
}
public void Dispose()
{
if (IsDataPacket)
{
_outboundMessageList.Clear();
MessageListPool.Return(_outboundMessageList);
}
_outboundMessageList = new();
}
/// <summary>
@ -74,6 +63,8 @@ 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);
@ -135,10 +126,15 @@ namespace MHServerEmu.Core.Network
if (_outboundMessageList.Count == 0)
return Logger.WarnReturn(false, "SerializeData(): Data packet contains no messages");
using RecyclableCodedOutputStream cos = RecyclableCodedOutputStream.CreateInstance(stream);
// Use pooled buffers for coded output streams with reflection hackery, see ProtobufHelper for more info.
byte[] buffer = BufferPool.Rent(4096);
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.Memory;
using MHServerEmu.Core.Metrics;
using MHServerEmu.Core.System.Time;
namespace MHServerEmu.Core.Network
@ -13,8 +13,9 @@ namespace MHServerEmu.Core.Network
Leaderboard,
PlayerManager,
GroupingManager,
Billing,
Frontend,
WebFrontend,
Auth,
NumServiceTypes
}
@ -233,30 +234,33 @@ 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 GetServerStatusString()
public string GetServerStatus(bool includeMetrics)
{
using var statusDictHandle = DictionaryPool<string, long>.Instance.Get(out Dictionary<string, long> statusDict);
GetServerStatus(statusDict);
StringBuilder sb = new();
foreach (var kvp in statusDict)
sb.AppendLine($"{kvp.Key}: {kvp.Value}");
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));
}
return sb.ToString();
}

View file

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

View file

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

View file

@ -345,22 +345,13 @@ 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);
try
{
packet.Serialize(buffer);
sent = await SendAsync(connection, buffer, size, flags);
}
finally
{
_bufferPool.Return(buffer);
packet.Dispose();
}
packet.Serialize(buffer);
int sent = await SendAsync(connection, buffer, size, flags);
_bufferPool.Return(buffer);
return sent;
}

View file

@ -1,21 +0,0 @@
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

@ -1,117 +0,0 @@
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

@ -1,130 +0,0 @@
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

@ -1,194 +0,0 @@
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

@ -1,158 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -19,48 +19,38 @@ 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,
// Versions 1-8 were used in the 0.x branch, so we start at 9 here.
Initial = 9,
Initial = 1,
AddedMissions = 2,
AddedVendorPurchaseData = 3,
ImplementedConditionPersistence = 4,
ImplementedLoginRewards = 5,
ImplementedMapDiscoveryDataPersistence = 6,
AddedRegionProtoRefToMapDiscoveryData = 7,
// Update the current version if you add any <---------
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
Current = AddedRegionProtoRefToMapDiscoveryData
}
/// <summary>
/// An implementation of the custom Gazillion serialization archive format.
/// </summary>
public sealed class Archive : IDisposable
public class Archive : IDisposable
{
private static readonly Logger Logger = LogManager.CreateLogger();
// Reuse the same buffers for all archives on the same game thread.
[ThreadStatic]
private static byte[] ReadBuffer;
[ThreadStatic]
private static byte[] WriteBuffer;
// Reuse the same buffers for all archives on the same thread. In practice this means one buffer instance of each type per game.
[ThreadStatic]
private static MemoryStream SharedAutoBuffer;
[ThreadStatic]
private static byte[] CodedOutputStreamBuffer;
private readonly MemoryStream _buffer; // MemoryStream replaces StreamAutoBuffer from the original implementation
private readonly MemoryStream _bufferStream; // MemoryStream replaces AutoBuffer 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;
@ -118,15 +108,20 @@ namespace MHServerEmu.Core.Serialization
if ((serializeType == ArchiveSerializeType.Replication || serializeType == ArchiveSerializeType.Database) == false)
throw new NotImplementedException($"Unsupported archive serialize type {serializeType}.");
InitializeBuffers();
// 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).
}
// Reuse the same stream for all packing archives
_buffer = SharedAutoBuffer;
if (_buffer.Length > 0)
_buffer.SetLength(0);
_bufferStream = SharedAutoBuffer;
if (_bufferStream.Length > 0)
_bufferStream.SetLength(0);
// Use reflection hackery to reuse the same buffer for all coded output streams, see ProtobufHelper for details.
_cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(_buffer, WriteBuffer);
_cos = ProtobufHelper.CodedOutputStreamEx.CreateInstance(_bufferStream, CodedOutputStreamBuffer);
SerializeType = serializeType;
ReplicationPolicy = replicationPolicy;
@ -144,10 +139,8 @@ namespace MHServerEmu.Core.Serialization
if ((serializeType == ArchiveSerializeType.Replication || serializeType == ArchiveSerializeType.Database) == false)
throw new NotImplementedException($"Unsupported archive serialize type {serializeType}.");
InitializeBuffers();
_buffer = new(buffer);
_cis = CodedInputStream.CreateInstance(_buffer, ReadBuffer);
_bufferStream = new(buffer);
_cis = CodedInputStream.CreateInstance(_bufferStream);
SerializeType = serializeType;
IsPacking = false;
@ -164,34 +157,13 @@ 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()
{
return _buffer;
}
public MemoryStream AccessAutoBuffer() => _bufferStream;
/// <summary>
/// Converts the underlying <see cref="MemoryStream"/> to <see cref="ByteString"/>.
@ -199,13 +171,7 @@ 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(_buffer.ToArray());
}
public Span<byte> AsSpan()
{
byte[] buffer = _buffer.GetBuffer();
return buffer.AsSpan(0, (int)CurrentOffset);
return ByteString.Unsafe.FromBytes(_bufferStream.ToArray());
}
/// <summary>
@ -217,14 +183,8 @@ 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)
{
@ -244,24 +204,9 @@ 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)
{
@ -308,7 +253,7 @@ namespace MHServerEmu.Core.Serialization
}
else
{
_buffer.WriteByteAt(_lastBitEncodedOffset, bitBuffer);
_bufferStream.WriteByteAt(_lastBitEncodedOffset, bitBuffer);
if (numEncodedBits >= 5)
_lastBitEncodedOffset = 0;
}
@ -661,8 +606,7 @@ namespace MHServerEmu.Core.Serialization
if (IsPacking)
{
WriteVarint(ioData);
UpdateSizeInArchive();
return WriteVarint(ioData);
}
else
{
@ -689,8 +633,7 @@ namespace MHServerEmu.Core.Serialization
if (IsPacking)
{
WriteVarint(ioData);
UpdateSizeInArchive();
return WriteVarint(ioData);
}
else
{
@ -732,7 +675,7 @@ namespace MHServerEmu.Core.Serialization
/// </summary>
private bool StartSizeChecking(ref long startPosition, ref uint size)
{
if (IsPersistent == false)
if (IsPersistent == false || Version < ArchiveVersion.AddedMissions)
return true;
// NOTE: COS/CIS are buffered, so we need to use their position, and not the one from the underlying stream.
@ -758,7 +701,7 @@ namespace MHServerEmu.Core.Serialization
/// </summary>
private bool EndSizeChecking(ref long startPosition, ref uint size, bool skip)
{
if (IsPersistent == false)
if (IsPersistent == false || Version < ArchiveVersion.AddedMissions)
return true;
if (IsPacking)
@ -786,40 +729,6 @@ 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
@ -831,9 +740,6 @@ namespace MHServerEmu.Core.Serialization
{
_cos.WriteRawByte(value);
_cos.Flush();
UpdateSizeInArchive();
return true;
}
@ -864,9 +770,6 @@ namespace MHServerEmu.Core.Serialization
_cos.WriteRawByte(@byte);
_cos.Flush();
UpdateSizeInArchive();
return true;
}
@ -903,7 +806,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 _buffer.WriteUInt32At(position, value);
return _bufferStream.WriteUInt32At(position, value);
}
/// <summary>
@ -1018,7 +921,7 @@ namespace MHServerEmu.Core.Serialization
{
if (_lastBitEncodedOffset == 0) return null;
if (_buffer.ReadByteAt(_lastBitEncodedOffset, out byte lastBitEncoded) == false)
if (_bufferStream.ReadByteAt(_lastBitEncodedOffset, out byte lastBitEncoded) == false)
{
SetError("Failed getting last bit encoded!");
return null;
@ -1075,5 +978,31 @@ 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

@ -1,18 +0,0 @@
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

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

View file

@ -1,71 +0,0 @@
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

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

View file

@ -1,93 +0,0 @@
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

@ -1,98 +0,0 @@
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(ref Orientation v)
public static bool IsFinite(Orientation v)
{
return float.IsFinite(v.Yaw) && float.IsFinite(v.Pitch) && float.IsFinite(v.Roll);
}

View file

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

View file

@ -38,18 +38,13 @@ 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 playerDbId, out string playerName);
public bool TryGetPlayerName(ulong id, 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>
@ -69,30 +64,5 @@ 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

@ -1,47 +0,0 @@
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,7 +67,6 @@ namespace MHServerEmu.DatabaseAccess.Json
public bool TryQueryAccountByEmail(string email, out DBAccount account)
{
account = _account;
account.MigrationData.Reset();
return true;
}
@ -89,12 +88,6 @@ 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");
@ -124,37 +117,6 @@ 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>1.0.0.0</AssemblyVersion>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
</PropertyGroup>
@ -20,8 +20,6 @@
<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>
@ -31,8 +29,6 @@
<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,13 +16,12 @@ namespace MHServerEmu.DatabaseAccess.Models
[Flags]
public enum AccountFlags
{
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,
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,
}
/// <summary>
@ -30,12 +29,8 @@ 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; }
@ -52,10 +47,6 @@ 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();
@ -97,66 +88,12 @@ 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,15 +16,12 @@ 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; }
@ -80,59 +77,6 @@ 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

@ -1,29 +0,0 @@
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

@ -1,21 +0,0 @@
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,48 +4,26 @@ 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;
// Properties for summoned entities need to be migrated, and these have arbitrary runtime dbIds, so just clear everything.
_properties.Clear();
PlayerProperties.Clear();
WorldView.Clear();
MatchQueueStatus = null;
CommunityStatus.Clear();
}
}

View file

@ -13,8 +13,7 @@ namespace MHServerEmu.DatabaseAccess.SQLite
/// </summary>
public class SQLiteDBManager : IDBManager
{
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 CurrentSchemaVersion = 4; // Increment this when making changes to the database schema
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
@ -38,10 +37,7 @@ namespace MHServerEmu.DatabaseAccess.SQLite
var config = ConfigManager.Instance.GetConfig<SQLiteDBManagerConfig>();
_dbFilePath = Path.Combine(FileHelper.DataDirectory, config.FileName);
_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.
_connectionString = $"Data Source={_dbFilePath};Synchronous=NORMAL;";
if (File.Exists(_dbFilePath) == false)
{
@ -66,10 +62,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 });
// 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 });
// Associated player data is loaded separately
account = accounts.FirstOrDefault();
return account != null;
}
@ -94,11 +90,11 @@ namespace MHServerEmu.DatabaseAccess.SQLite
return true;
}
public bool TryGetPlayerName(ulong playerDbId, out string playerName)
public bool TryGetPlayerName(ulong id, out string playerName)
{
using SQLiteConnection connection = GetConnection();
playerName = connection.QueryFirstOrDefault<string>("SELECT PlayerName FROM Account WHERE Id = @Id", new { Id = (long)playerDbId });
playerName = connection.QueryFirstOrDefault<string>("SELECT PlayerName FROM Account WHERE Id = @Id", new { Id = (long)id });
return string.IsNullOrWhiteSpace(playerName) == false;
}
@ -115,15 +111,6 @@ 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)
@ -217,129 +204,6 @@ 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>
@ -398,18 +262,6 @@ 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)
@ -471,9 +323,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, LastLogoutTime=@LastLogoutTime 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 WHERE DbGuid = @DbGuid",
account.Player, transaction);
}
else

View file

@ -67,19 +67,24 @@ namespace MHServerEmu.DatabaseAccess.SQLite
public void UpdateEntities(SQLiteConnection connection, SQLiteTransaction transaction, long containerDbGuid, DBEntityCollection dbEntityCollection)
{
// Delete items that no longer belong to this account
using var entitiesToDeleteHandle = ListPool<long>.Instance.Get(out List<long> entitiesToDelete);
List<long> entitiesToDelete = ListPool<long>.Instance.Get();
GetEntitiesToDelete(connection, containerDbGuid, dbEntityCollection, entitiesToDelete);
if (entitiesToDelete.Count > 0)
connection.Execute(_deleteQuery, new { EntitiesToDelete = 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);
}
// Insert and update
IReadOnlyList<DBEntity> entries = dbEntityCollection.GetEntriesForContainer(containerDbGuid);
if (entries.Count > 0)
{
connection.Execute(_insertQuery, entries, transaction);
connection.Execute(_updateQuery, entries, transaction);
}
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=6;
PRAGMA user_version=4;
PRAGMA journal_mode=WAL;
CREATE TABLE "Account" (
@ -20,7 +20,6 @@ CREATE TABLE "Player" (
"StartTarget" INTEGER,
"AOIVolume" INTEGER,
"GazillioniteBalance" INTEGER,
"LastLogoutTime" INTEGER,
FOREIGN KEY("DbGuid") REFERENCES "Account"("Id") ON DELETE CASCADE,
PRIMARY KEY("DbGuid")
);
@ -71,24 +70,6 @@ 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

@ -1,26 +0,0 @@
-- 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

@ -1 +0,0 @@
-- 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.RateLimiting;
using MHServerEmu.Core.System;
using MHServerEmu.DatabaseAccess;
using MHServerEmu.DatabaseAccess.Models;
@ -14,13 +14,14 @@ namespace MHServerEmu.Frontend
/// </summary>
public class FrontendClient : TcpClient, IFrontendClient, IDBAccountOwner
{
// Rate limit at 8 KB/s with a bit of burst allowed.
private const int RateLimitBytesPerSecond = 1024 * 8;
private const int RateLimitBurst = RateLimitBytesPerSecond * 10;
// 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;
private static readonly Logger Logger = LogManager.CreateLogger();
private readonly TokenBucket _tokenBucket = new(RateLimitBytesPerSecond, RateLimitBurst);
private readonly TokenBucket _tokenBucket = new(RateLimitPacketsPerSecond, RateLimitBurst);
private readonly MuxReader _muxReader;
// We intentionally don't use an array here so that channel state is inlined in FrontendClient
@ -137,7 +138,7 @@ namespace MHServerEmu.Frontend
/// </summary>
public void HandleIncomingData(byte[] buffer, int length)
{
if (_tokenBucket.CheckLimit(length) == false)
if (_tokenBucket.CheckLimit() == false)
{
Logger.Error($"HandleIncomingData(): Rate limit exceeded for client [{this}]");
Disconnect();
@ -240,10 +241,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.
// 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);
// 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);
return true;
}

View file

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

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