Initial commit

This commit is contained in:
BillyCool
2026-04-21 01:10:25 +10:00
commit c5595ea083
1752 changed files with 45767 additions and 0 deletions

409
.gitignore vendored Normal file
View File

@@ -0,0 +1,409 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
# Squad
.gitattributes
.github
.copilot
.squad
# User Data
src/Data/UserData
.squad-workstream
src/Data/MasterData

37
DISCLAIMER.md Normal file
View File

@@ -0,0 +1,37 @@
# Disclaimer
## No Affiliation
This project is an unofficial, fan-made private server reimplementation for NieR: Reincarnation. It is not affiliated with, endorsed by, or associated with Square Enix Co., Ltd., Applibot, Inc., or any of their subsidiaries or affiliates in any way.
NieR: Reincarnation, NieR, and all related names, characters, logos, and content are trademarks and intellectual property of Square Enix Co., Ltd. and/or Applibot, Inc. All rights reserved by their respective owners.
## Purpose
This project exists solely for **preservation and educational purposes**. NieR: Reincarnation's official servers were shut down on April 30, 2024, making the game otherwise unplayable. This project aims to allow fans to continue experiencing a game they love, with no intent to harm the interests of the original creators.
## Non-Commercial
This project is and will remain entirely **non-profit**. No fees are charged for access. No donations are solicited in connection with this project. No attempt is made to monetise the NieR: Reincarnation name, brand, or any associated intellectual property.
## License Scope
The [MIT License](./LICENSE) applying to this repository covers **only the original server implementation code** written by the contributors of this project. It does not and cannot grant any rights over NieR: Reincarnation's game assets, artwork, audio, story, characters, data, or any other intellectual property owned by Square Enix or Applibot.
**No game assets are included in this repository.** This includes but is not limited to: APK files, images, audio files, video files, and game data files. Users are responsible for obtaining any necessary game files through legitimate means.
## API Definitions and Data Models
The protocol buffer service definitions and data model classes in this repository represent functional API structure — message shapes, field identifiers, and service method signatures — derived from observing network traffic between the game client and its servers, and from inspection of publicly exposed type signatures and namespaces. They were not copied or extracted from any proprietary source files. Functional interfaces of this nature are not protectable expression under applicable copyright law.
## Interoperability
Where any circumvention of technical protection measures was necessary to achieve interoperability with this independently created server software, such acts were undertaken solely for that purpose pursuant to 17 U.S.C. §1201(f) (interoperability exception) and equivalent provisions under applicable law. No such circumvention was undertaken for any other purpose.
## No Warranty
This software is provided as-is, with no warranty of any kind. See the [MIT License](./LICENSE) for full terms.
## Takedown Requests
If you are a representative of Square Enix Co., Ltd. or Applibot, Inc. and have concerns about this project, please open an issue or contact the repository maintainers directly. We will respond promptly and in good faith.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 BillyCool
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
MariesWonderland.slnx Normal file
View File

@@ -0,0 +1,4 @@
<Solution>
<Project Path="src\MariesWonderland.csproj" />
<Project Path="tests\MariesWonderland.Tests.csproj" />
</Solution>

76
README.md Normal file
View File

@@ -0,0 +1,76 @@
# Marie's Wonderland
An open-source server implementation for mobile game NieR Reincarnation.
## Requirements
#### PC
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
- [Visual Studio 2026](https://visualstudio.microsoft.com/downloads) or [Rider](https://www.jetbrains.com/rider/)
- [Android Platform Tools](https://developer.android.com/tools/releases/platform-tools) (`adb`)
#### Phone
- An Android device or an [Android Studio](https://developer.android.com/studio) emulator (physical device not required)
## Setup overview
### 1. Run the server
Open `MariesWonderland.slnx` and run the project.
The server listens on the standard HTTP and HTTPS ports on localhost:
- `http://localhost` (port 80) - used for HTTP asset serving
- `https://localhost` (port 443) - used for gRPC (HTTP/2)
### 2. Expose the server
The game communicates over gRPC (HTTP/2). You do not need ngrok or an external tunnel if your emulator or device can reach your machine directly.
- If running on an emulator: configure the emulator to reach the host (Android Studio emulators usually can reach the host).
- If running from a remote device or across a network: open ports 80 and 443 on your firewall/NAT and ensure those ports are forwarded to the machine running the server so the game can reach `http(s)://<your-host>`.
Ensure any network path supports HTTP/2 for gRPC traffic on port 443.
## Configuration
Server settings live in `src/appsettings.development.json`:
```json
{
"Server": {
"Paths": {
"AssetDatabase": "<path to extracted asset revisions>",
"MasterDatabase": "<path to extracted master data>",
"ResourcesBaseUrl": "http://<your-host>/aaaaaaaaaaaaaaaaaaaaaaaa"
},
"Data": {
"LatestMasterDataVersion": "1234567890",
"UserDataPath": "Data/UserData"
}
}
}
```
- The `ResourcesBaseUrl` value must be exactly 43 characters long.
- If you change the length of that segment, you may also need to update the server-side minimal API that serves the short path (the `/aaaaaaaa...` handler) so its expected length matches your new value.
## Project structure
```
src/ .NET 10 gRPC + HTTP server
proto/ protobuf service definitions
Services/ gRPC service implementations
Data/ in-memory data stores (master + user)
Models/ entity and type definitions
Extensions/ DI, HTTP, and gRPC helpers
Configuration/ strongly-typed options
Http/ HTTP API handlers (asset serving, etc.)
Interceptors/ gRPC interceptors (diff, logging, auth)
Helpers/ shared game logic helpers
tests/ xUnit test project
Infrastructure/ shared test base classes and fixtures
Interceptors/ interceptor unit tests
```
## Disclaimer
See [DISCLAIMER.md](DISCLAIMER.md).
## Special Thanks
- [onepiecefreak3](https://github.com/onepiecefreak3)
- [Walter-Sparrow](https://github.com/Walter-Sparrow)

View File

@@ -0,0 +1,34 @@
namespace MariesWonderland.Configuration;
public sealed class ServerOptions
{
public const string SectionName = "Server";
public PathsOptions Paths { get; init; } = new();
public DataOptions Data { get; init; } = new();
}
public sealed class PathsOptions
{
public string AssetDatabase { get; init; } = string.Empty;
public string MasterDatabase { get; init; } = string.Empty;
/// <summary>
/// Replacement URL written into list.bin in-place when serving asset lists.
/// Must be exactly 43 ASCII bytes to preserve protobuf field lengths.
/// Leave empty to serve list.bin unmodified.
/// </summary>
public string ResourcesBaseUrl { get; init; } = string.Empty;
}
public sealed class DataOptions
{
public string LatestMasterDataVersion { get; init; } = string.Empty;
public string UserDataPath { get; init; } = string.Empty;
/// <summary>
/// Path to the JSON file used to persist user data between server restarts.
/// If relative, resolved against the application base directory.
/// </summary>
public string UserDatabase { get; init; } = "userdata.json";
}

18
src/Constants.cs Normal file
View File

@@ -0,0 +1,18 @@
namespace MariesWonderland;
public static class Constants
{
public const long MinPlayerId = 1_000_000_000_000L; // 1e12, 1 trillion
public const long MaxPlayerId = 2_000_000_000_000L; // 2e12, 2 trillion
public const long MinUserId = 1_000_000_000_000_000_000L; // 1e18, 1 quintillion
public const long MaxUserId = 2_000_000_000_000_000_000L; // 2e18, 2 quintillion
public static readonly List<int> StartingWeaponIds = [100001, 100011, 100021]; // Deathpierce Sword, Deathshot Pistol, Deathstrike Staff,
public const int StartingDeckCostumeId = 10100; // Rion
public const int StartingDeckWeaponId = 101001; // Everlasting Cardia
}

View File

@@ -0,0 +1,686 @@
using MariesWonderland.MasterMemory;
using MariesWonderland.Models.Entities;
using MessagePack;
using MessagePack.Formatters;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
namespace MariesWonderland.Data;
/// <summary>
/// Loads all master data tables from the encrypted binary .bin.e database file.
/// The binary format is AES-128-CBC encrypted MessagePack with LZ4Block per-table compression.
/// </summary>
public static class BinaryMasterDataLoader
{
private static readonly byte[] AesKey = Encoding.UTF8.GetBytes("6Cb01321EE5e6bBe");
private static readonly byte[] AesIv = Encoding.UTF8.GetBytes("EfcAef4CAe5f6DaA");
/// <summary>
/// Reads and decrypts the .bin.e file at <paramref name="binFilePath"/>, deserializes
/// all 607 master data tables, and returns a populated <see cref="DarkMasterMemoryDatabase"/>.
/// </summary>
public static DarkMasterMemoryDatabase Load(string binFilePath)
{
var encrypted = File.ReadAllBytes(binFilePath);
var decrypted = Decrypt(encrypted);
return Parse(decrypted);
}
/// <summary>Decrypts the raw .bin.e file bytes using AES-128-CBC.</summary>
private static byte[] Decrypt(byte[] data)
{
using var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Key = AesKey;
aes.IV = AesIv;
return aes.CreateDecryptor().TransformFinalBlock(data, 0, data.Length);
}
/// <summary>
/// Parses the decrypted MessagePack binary: reads the table-offset header, then
/// deserializes each EntityM* table from its slice.
/// </summary>
private static DarkMasterMemoryDatabase Parse(byte[] data)
{
// Parse the header: Dictionary<string, (offset, length)> mapping table names to data positions
var reader = new MessagePackReader(data);
var formatter = new DictionaryFormatter<string, (int, int)>();
var header = formatter.Deserialize(ref reader, HeaderFormatterResolver.StandardOptions)!;
// Data section starts immediately after the header
var db = data.AsMemory((int)reader.Consumed);
var options = MessagePackSerializer.DefaultOptions
.WithResolver(new InternStringResolver(MessagePackSerializer.DefaultOptions.Resolver))
.WithCompression(MessagePackCompression.Lz4Block);
return new DarkMasterMemoryDatabase
{
EntityMAbilityBehaviourActionBless = Extract<EntityMAbilityBehaviourActionBless>(header, db, options),
EntityMAbilityBehaviourActionPassiveSkill = Extract<EntityMAbilityBehaviourActionPassiveSkill>(header, db, options),
EntityMAbilityBehaviourActionStatusDown = Extract<EntityMAbilityBehaviourActionStatusDown>(header, db, options),
EntityMAbilityBehaviourActionStatus = Extract<EntityMAbilityBehaviourActionStatus>(header, db, options),
EntityMAbilityBehaviourGroup = Extract<EntityMAbilityBehaviourGroup>(header, db, options),
EntityMAbilityBehaviour = Extract<EntityMAbilityBehaviour>(header, db, options),
EntityMAbilityDetail = Extract<EntityMAbilityDetail>(header, db, options),
EntityMAbilityLevelGroup = Extract<EntityMAbilityLevelGroup>(header, db, options),
EntityMAbilityStatus = Extract<EntityMAbilityStatus>(header, db, options),
EntityMAbility = Extract<EntityMAbility>(header, db, options),
EntityMActorAnimationCategory = Extract<EntityMActorAnimationCategory>(header, db, options),
EntityMActorAnimationController = Extract<EntityMActorAnimationController>(header, db, options),
EntityMActorAnimation = Extract<EntityMActorAnimation>(header, db, options),
EntityMActorObject = Extract<EntityMActorObject>(header, db, options),
EntityMActor = Extract<EntityMActor>(header, db, options),
EntityMAppealDialog = Extract<EntityMAppealDialog>(header, db, options),
EntityMAssetBackground = Extract<EntityMAssetBackground>(header, db, options),
EntityMAssetCalculator = Extract<EntityMAssetCalculator>(header, db, options),
EntityMAssetDataSetting = Extract<EntityMAssetDataSetting>(header, db, options),
EntityMAssetEffect = Extract<EntityMAssetEffect>(header, db, options),
EntityMAssetGradeIcon = Extract<EntityMAssetGradeIcon>(header, db, options),
EntityMAssetTimeline = Extract<EntityMAssetTimeline>(header, db, options),
EntityMAssetTurnbattlePrefab = Extract<EntityMAssetTurnbattlePrefab>(header, db, options),
EntityMBattleActorAi = Extract<EntityMBattleActorAi>(header, db, options),
EntityMBattleActorSkillAiGroup = Extract<EntityMBattleActorSkillAiGroup>(header, db, options),
EntityMBattleAdditionalAbility = Extract<EntityMBattleAdditionalAbility>(header, db, options),
EntityMBattleAttributeDamageCoefficientDefine = Extract<EntityMBattleAttributeDamageCoefficientDefine>(header, db, options),
EntityMBattleAttributeDamageCoefficientGroup = Extract<EntityMBattleAttributeDamageCoefficientGroup>(header, db, options),
EntityMBattleBgmSetGroup = Extract<EntityMBattleBgmSetGroup>(header, db, options),
EntityMBattleBgmSet = Extract<EntityMBattleBgmSet>(header, db, options),
EntityMBattleBigHuntDamageThresholdGroup = Extract<EntityMBattleBigHuntDamageThresholdGroup>(header, db, options),
EntityMBattleBigHuntKnockDownGaugeValueConfigGroup = Extract<EntityMBattleBigHuntKnockDownGaugeValueConfigGroup>(header, db, options),
EntityMBattleBigHuntPhaseGroup = Extract<EntityMBattleBigHuntPhaseGroup>(header, db, options),
EntityMBattleBigHunt = Extract<EntityMBattleBigHunt>(header, db, options),
EntityMBattleCompanionSkillAiGroup = Extract<EntityMBattleCompanionSkillAiGroup>(header, db, options),
EntityMBattleCostumeSkillFireAct = Extract<EntityMBattleCostumeSkillFireAct>(header, db, options),
EntityMBattleCostumeSkillSe = Extract<EntityMBattleCostumeSkillSe>(header, db, options),
EntityMBattleDropReward = Extract<EntityMBattleDropReward>(header, db, options),
EntityMBattleEnemySizeTypeConfig = Extract<EntityMBattleEnemySizeTypeConfig>(header, db, options),
EntityMBattleEventGroup = Extract<EntityMBattleEventGroup>(header, db, options),
EntityMBattleEventReceiverBehaviourGroup = Extract<EntityMBattleEventReceiverBehaviourGroup>(header, db, options),
EntityMBattleEventReceiverBehaviourHudActSequence = Extract<EntityMBattleEventReceiverBehaviourHudActSequence>(header, db, options),
EntityMBattleEventReceiverBehaviourRadioMessage = Extract<EntityMBattleEventReceiverBehaviourRadioMessage>(header, db, options),
EntityMBattleEvent = Extract<EntityMBattleEvent>(header, db, options),
EntityMBattleEventTriggerBehaviourBattleStart = Extract<EntityMBattleEventTriggerBehaviourBattleStart>(header, db, options),
EntityMBattleEventTriggerBehaviourGroup = Extract<EntityMBattleEventTriggerBehaviourGroup>(header, db, options),
EntityMBattleEventTriggerBehaviourWaveStart = Extract<EntityMBattleEventTriggerBehaviourWaveStart>(header, db, options),
EntityMBattleGeneralViewConfiguration = Extract<EntityMBattleGeneralViewConfiguration>(header, db, options),
EntityMBattleGroup = Extract<EntityMBattleGroup>(header, db, options),
EntityMBattleNpcCharacterBoardAbility = Extract<EntityMBattleNpcCharacterBoardAbility>(header, db, options),
EntityMBattleNpcCharacterBoardCompleteReward = Extract<EntityMBattleNpcCharacterBoardCompleteReward>(header, db, options),
EntityMBattleNpcCharacterBoardStatusUp = Extract<EntityMBattleNpcCharacterBoardStatusUp>(header, db, options),
EntityMBattleNpcCharacterBoard = Extract<EntityMBattleNpcCharacterBoard>(header, db, options),
EntityMBattleNpcCharacterCostumeLevelBonus = Extract<EntityMBattleNpcCharacterCostumeLevelBonus>(header, db, options),
EntityMBattleNpcCharacterRebirth = Extract<EntityMBattleNpcCharacterRebirth>(header, db, options),
EntityMBattleNpcCharacter = Extract<EntityMBattleNpcCharacter>(header, db, options),
EntityMBattleNpcCharacterViewerField = Extract<EntityMBattleNpcCharacterViewerField>(header, db, options),
EntityMBattleNpcCompanion = Extract<EntityMBattleNpcCompanion>(header, db, options),
EntityMBattleNpcCostumeActiveSkill = Extract<EntityMBattleNpcCostumeActiveSkill>(header, db, options),
EntityMBattleNpcCostumeAwakenStatusUp = Extract<EntityMBattleNpcCostumeAwakenStatusUp>(header, db, options),
EntityMBattleNpcCostumeLevelBonusReevaluate = Extract<EntityMBattleNpcCostumeLevelBonusReevaluate>(header, db, options),
EntityMBattleNpcCostumeLevelBonusReleaseStatus = Extract<EntityMBattleNpcCostumeLevelBonusReleaseStatus>(header, db, options),
EntityMBattleNpcCostumeLotteryEffectAbility = Extract<EntityMBattleNpcCostumeLotteryEffectAbility>(header, db, options),
EntityMBattleNpcCostumeLotteryEffectPending = Extract<EntityMBattleNpcCostumeLotteryEffectPending>(header, db, options),
EntityMBattleNpcCostumeLotteryEffectStatusUp = Extract<EntityMBattleNpcCostumeLotteryEffectStatusUp>(header, db, options),
EntityMBattleNpcCostumeLotteryEffect = Extract<EntityMBattleNpcCostumeLotteryEffect>(header, db, options),
EntityMBattleNpcCostume = Extract<EntityMBattleNpcCostume>(header, db, options),
EntityMBattleNpcDeckBackup = Extract<EntityMBattleNpcDeckBackup>(header, db, options),
EntityMBattleNpcDeckCharacterDressupCostume = Extract<EntityMBattleNpcDeckCharacterDressupCostume>(header, db, options),
EntityMBattleNpcDeckCharacterDropCategory = Extract<EntityMBattleNpcDeckCharacterDropCategory>(header, db, options),
EntityMBattleNpcDeckCharacter = Extract<EntityMBattleNpcDeckCharacter>(header, db, options),
EntityMBattleNpcDeckCharacterType = Extract<EntityMBattleNpcDeckCharacterType>(header, db, options),
EntityMBattleNpcDeckLimitContentBackupRestored = Extract<EntityMBattleNpcDeckLimitContentBackupRestored>(header, db, options),
EntityMBattleNpcDeckLimitContentBackup = Extract<EntityMBattleNpcDeckLimitContentBackup>(header, db, options),
EntityMBattleNpcDeckLimitContentDeletedCharacter = Extract<EntityMBattleNpcDeckLimitContentDeletedCharacter>(header, db, options),
EntityMBattleNpcDeckLimitContentRestricted = Extract<EntityMBattleNpcDeckLimitContentRestricted>(header, db, options),
EntityMBattleNpcDeckPartsGroup = Extract<EntityMBattleNpcDeckPartsGroup>(header, db, options),
EntityMBattleNpcDeckSubWeaponGroup = Extract<EntityMBattleNpcDeckSubWeaponGroup>(header, db, options),
EntityMBattleNpcDeck = Extract<EntityMBattleNpcDeck>(header, db, options),
EntityMBattleNpcDeckTypeNote = Extract<EntityMBattleNpcDeckTypeNote>(header, db, options),
EntityMBattleNpcPartsGroupNote = Extract<EntityMBattleNpcPartsGroupNote>(header, db, options),
EntityMBattleNpcPartsPreset = Extract<EntityMBattleNpcPartsPreset>(header, db, options),
EntityMBattleNpcPartsPresetTag = Extract<EntityMBattleNpcPartsPresetTag>(header, db, options),
EntityMBattleNpcPartsStatusSub = Extract<EntityMBattleNpcPartsStatusSub>(header, db, options),
EntityMBattleNpcParts = Extract<EntityMBattleNpcParts>(header, db, options),
EntityMBattleNpcSpecialEndAct = Extract<EntityMBattleNpcSpecialEndAct>(header, db, options),
EntityMBattleNpc = Extract<EntityMBattleNpc>(header, db, options),
EntityMBattleNpcWeaponAbilityReevaluate = Extract<EntityMBattleNpcWeaponAbilityReevaluate>(header, db, options),
EntityMBattleNpcWeaponAbility = Extract<EntityMBattleNpcWeaponAbility>(header, db, options),
EntityMBattleNpcWeaponAwaken = Extract<EntityMBattleNpcWeaponAwaken>(header, db, options),
EntityMBattleNpcWeaponNoteReevaluate = Extract<EntityMBattleNpcWeaponNoteReevaluate>(header, db, options),
EntityMBattleNpcWeaponNote = Extract<EntityMBattleNpcWeaponNote>(header, db, options),
EntityMBattleNpcWeaponSkill = Extract<EntityMBattleNpcWeaponSkill>(header, db, options),
EntityMBattleNpcWeaponStoryReevaluate = Extract<EntityMBattleNpcWeaponStoryReevaluate>(header, db, options),
EntityMBattleNpcWeaponStory = Extract<EntityMBattleNpcWeaponStory>(header, db, options),
EntityMBattleNpcWeapon = Extract<EntityMBattleNpcWeapon>(header, db, options),
EntityMBattleProgressUiType = Extract<EntityMBattleProgressUiType>(header, db, options),
EntityMBattleQuestSceneBgmSetGroup = Extract<EntityMBattleQuestSceneBgmSetGroup>(header, db, options),
EntityMBattleQuestSceneBgm = Extract<EntityMBattleQuestSceneBgm>(header, db, options),
EntityMBattleRentalDeck = Extract<EntityMBattleRentalDeck>(header, db, options),
EntityMBattleSkillBehaviourHitDamageConfiguration = Extract<EntityMBattleSkillBehaviourHitDamageConfiguration>(header, db, options),
EntityMBattleSkillFireActConditionAttributeType = Extract<EntityMBattleSkillFireActConditionAttributeType>(header, db, options),
EntityMBattleSkillFireActConditionGroup = Extract<EntityMBattleSkillFireActConditionGroup>(header, db, options),
EntityMBattleSkillFireActConditionSkillCategoryType = Extract<EntityMBattleSkillFireActConditionSkillCategoryType>(header, db, options),
EntityMBattleSkillFireActConditionWeaponType = Extract<EntityMBattleSkillFireActConditionWeaponType>(header, db, options),
EntityMBattleSkillFireAct = Extract<EntityMBattleSkillFireAct>(header, db, options),
EntityMBattle = Extract<EntityMBattle>(header, db, options),
EntityMBeginnerCampaign = Extract<EntityMBeginnerCampaign>(header, db, options),
EntityMBigHuntBossGradeGroupAttribute = Extract<EntityMBigHuntBossGradeGroupAttribute>(header, db, options),
EntityMBigHuntBossGradeGroup = Extract<EntityMBigHuntBossGradeGroup>(header, db, options),
EntityMBigHuntBossQuestGroupChallengeCategory = Extract<EntityMBigHuntBossQuestGroupChallengeCategory>(header, db, options),
EntityMBigHuntBossQuestGroup = Extract<EntityMBigHuntBossQuestGroup>(header, db, options),
EntityMBigHuntBossQuest = Extract<EntityMBigHuntBossQuest>(header, db, options),
EntityMBigHuntBoss = Extract<EntityMBigHuntBoss>(header, db, options),
EntityMBigHuntLink = Extract<EntityMBigHuntLink>(header, db, options),
EntityMBigHuntQuestGroup = Extract<EntityMBigHuntQuestGroup>(header, db, options),
EntityMBigHuntQuestScoreCoefficient = Extract<EntityMBigHuntQuestScoreCoefficient>(header, db, options),
EntityMBigHuntQuest = Extract<EntityMBigHuntQuest>(header, db, options),
EntityMBigHuntRewardGroup = Extract<EntityMBigHuntRewardGroup>(header, db, options),
EntityMBigHuntSchedule = Extract<EntityMBigHuntSchedule>(header, db, options),
EntityMBigHuntScoreRewardGroupSchedule = Extract<EntityMBigHuntScoreRewardGroupSchedule>(header, db, options),
EntityMBigHuntScoreRewardGroup = Extract<EntityMBigHuntScoreRewardGroup>(header, db, options),
EntityMBigHuntWeeklyAttributeScoreRewardGroupSchedule = Extract<EntityMBigHuntWeeklyAttributeScoreRewardGroupSchedule>(header, db, options),
EntityMCageMemory = Extract<EntityMCageMemory>(header, db, options),
EntityMCageOrnamentMainQuestChapterStill = Extract<EntityMCageOrnamentMainQuestChapterStill>(header, db, options),
EntityMCageOrnamentReward = Extract<EntityMCageOrnamentReward>(header, db, options),
EntityMCageOrnamentStillReleaseCondition = Extract<EntityMCageOrnamentStillReleaseCondition>(header, db, options),
EntityMCageOrnament = Extract<EntityMCageOrnament>(header, db, options),
EntityMCatalogCompanion = Extract<EntityMCatalogCompanion>(header, db, options),
EntityMCatalogCostume = Extract<EntityMCatalogCostume>(header, db, options),
EntityMCatalogPartsGroup = Extract<EntityMCatalogPartsGroup>(header, db, options),
EntityMCatalogTerm = Extract<EntityMCatalogTerm>(header, db, options),
EntityMCatalogThought = Extract<EntityMCatalogThought>(header, db, options),
EntityMCatalogWeapon = Extract<EntityMCatalogWeapon>(header, db, options),
EntityMCharacterBoardAbilityMaxLevel = Extract<EntityMCharacterBoardAbilityMaxLevel>(header, db, options),
EntityMCharacterBoardAbility = Extract<EntityMCharacterBoardAbility>(header, db, options),
EntityMCharacterBoardAssignment = Extract<EntityMCharacterBoardAssignment>(header, db, options),
EntityMCharacterBoardCategory = Extract<EntityMCharacterBoardCategory>(header, db, options),
EntityMCharacterBoardCompleteRewardGroup = Extract<EntityMCharacterBoardCompleteRewardGroup>(header, db, options),
EntityMCharacterBoardCompleteReward = Extract<EntityMCharacterBoardCompleteReward>(header, db, options),
EntityMCharacterBoardConditionDetail = Extract<EntityMCharacterBoardConditionDetail>(header, db, options),
EntityMCharacterBoardConditionGroup = Extract<EntityMCharacterBoardConditionGroup>(header, db, options),
EntityMCharacterBoardConditionIgnore = Extract<EntityMCharacterBoardConditionIgnore>(header, db, options),
EntityMCharacterBoardCondition = Extract<EntityMCharacterBoardCondition>(header, db, options),
EntityMCharacterBoardEffectTargetGroup = Extract<EntityMCharacterBoardEffectTargetGroup>(header, db, options),
EntityMCharacterBoardGroup = Extract<EntityMCharacterBoardGroup>(header, db, options),
EntityMCharacterBoardPanelReleaseEffectGroup = Extract<EntityMCharacterBoardPanelReleaseEffectGroup>(header, db, options),
EntityMCharacterBoardPanelReleasePossessionGroup = Extract<EntityMCharacterBoardPanelReleasePossessionGroup>(header, db, options),
EntityMCharacterBoardPanelReleaseRewardGroup = Extract<EntityMCharacterBoardPanelReleaseRewardGroup>(header, db, options),
EntityMCharacterBoardPanel = Extract<EntityMCharacterBoardPanel>(header, db, options),
EntityMCharacterBoardStatusUp = Extract<EntityMCharacterBoardStatusUp>(header, db, options),
EntityMCharacterBoard = Extract<EntityMCharacterBoard>(header, db, options),
EntityMCharacterDisplaySwitch = Extract<EntityMCharacterDisplaySwitch>(header, db, options),
EntityMCharacterLevelBonusAbilityGroup = Extract<EntityMCharacterLevelBonusAbilityGroup>(header, db, options),
EntityMCharacterRebirthMaterialGroup = Extract<EntityMCharacterRebirthMaterialGroup>(header, db, options),
EntityMCharacterRebirthStepGroup = Extract<EntityMCharacterRebirthStepGroup>(header, db, options),
EntityMCharacterRebirth = Extract<EntityMCharacterRebirth>(header, db, options),
EntityMCharacter = Extract<EntityMCharacter>(header, db, options),
EntityMCharacterViewerActorIcon = Extract<EntityMCharacterViewerActorIcon>(header, db, options),
EntityMCharacterViewerFieldSettings = Extract<EntityMCharacterViewerFieldSettings>(header, db, options),
EntityMCharacterViewerField = Extract<EntityMCharacterViewerField>(header, db, options),
EntityMCharacterVoiceUnlockCondition = Extract<EntityMCharacterVoiceUnlockCondition>(header, db, options),
EntityMCollectionBonusEffect = Extract<EntityMCollectionBonusEffect>(header, db, options),
EntityMCollectionBonusQuestAssignmentGroup = Extract<EntityMCollectionBonusQuestAssignmentGroup>(header, db, options),
EntityMCollectionBonusQuestAssignment = Extract<EntityMCollectionBonusQuestAssignment>(header, db, options),
EntityMComboCalculationSetting = Extract<EntityMComboCalculationSetting>(header, db, options),
EntityMComebackCampaign = Extract<EntityMComebackCampaign>(header, db, options),
EntityMCompanionAbilityGroup = Extract<EntityMCompanionAbilityGroup>(header, db, options),
EntityMCompanionAbilityLevel = Extract<EntityMCompanionAbilityLevel>(header, db, options),
EntityMCompanionBaseStatus = Extract<EntityMCompanionBaseStatus>(header, db, options),
EntityMCompanionCategory = Extract<EntityMCompanionCategory>(header, db, options),
EntityMCompanionDuplicationExchangePossessionGroup = Extract<EntityMCompanionDuplicationExchangePossessionGroup>(header, db, options),
EntityMCompanionEnhanced = Extract<EntityMCompanionEnhanced>(header, db, options),
EntityMCompanionEnhancementMaterial = Extract<EntityMCompanionEnhancementMaterial>(header, db, options),
EntityMCompanionSkillLevel = Extract<EntityMCompanionSkillLevel>(header, db, options),
EntityMCompanionStatusCalculation = Extract<EntityMCompanionStatusCalculation>(header, db, options),
EntityMCompanion = Extract<EntityMCompanion>(header, db, options),
EntityMCompleteMissionGroup = Extract<EntityMCompleteMissionGroup>(header, db, options),
EntityMConfig = Extract<EntityMConfig>(header, db, options),
EntityMConsumableItemEffect = Extract<EntityMConsumableItemEffect>(header, db, options),
EntityMConsumableItem = Extract<EntityMConsumableItem>(header, db, options),
EntityMConsumableItemTerm = Extract<EntityMConsumableItemTerm>(header, db, options),
EntityMContentsStory = Extract<EntityMContentsStory>(header, db, options),
EntityMCostumeAbilityGroup = Extract<EntityMCostumeAbilityGroup>(header, db, options),
EntityMCostumeAbilityLevelGroup = Extract<EntityMCostumeAbilityLevelGroup>(header, db, options),
EntityMCostumeActiveSkillEnhancementMaterial = Extract<EntityMCostumeActiveSkillEnhancementMaterial>(header, db, options),
EntityMCostumeActiveSkillGroup = Extract<EntityMCostumeActiveSkillGroup>(header, db, options),
EntityMCostumeAnimationStep = Extract<EntityMCostumeAnimationStep>(header, db, options),
EntityMCostumeAutoOrganizationCondition = Extract<EntityMCostumeAutoOrganizationCondition>(header, db, options),
EntityMCostumeAwakenAbility = Extract<EntityMCostumeAwakenAbility>(header, db, options),
EntityMCostumeAwakenEffectGroup = Extract<EntityMCostumeAwakenEffectGroup>(header, db, options),
EntityMCostumeAwakenItemAcquire = Extract<EntityMCostumeAwakenItemAcquire>(header, db, options),
EntityMCostumeAwakenMaterialGroup = Extract<EntityMCostumeAwakenMaterialGroup>(header, db, options),
EntityMCostumeAwakenPriceGroup = Extract<EntityMCostumeAwakenPriceGroup>(header, db, options),
EntityMCostumeAwakenStatusUpGroup = Extract<EntityMCostumeAwakenStatusUpGroup>(header, db, options),
EntityMCostumeAwakenStepMaterialGroup = Extract<EntityMCostumeAwakenStepMaterialGroup>(header, db, options),
EntityMCostumeAwaken = Extract<EntityMCostumeAwaken>(header, db, options),
EntityMCostumeBaseStatus = Extract<EntityMCostumeBaseStatus>(header, db, options),
EntityMCostumeCollectionBonusGroup = Extract<EntityMCostumeCollectionBonusGroup>(header, db, options),
EntityMCostumeCollectionBonus = Extract<EntityMCostumeCollectionBonus>(header, db, options),
EntityMCostumeDefaultSkillGroup = Extract<EntityMCostumeDefaultSkillGroup>(header, db, options),
EntityMCostumeDefaultSkillLotteryGroup = Extract<EntityMCostumeDefaultSkillLotteryGroup>(header, db, options),
EntityMCostumeDelete = Extract<EntityMCostumeDelete>(header, db, options),
EntityMCostumeDisplayCoordinateAdjustment = Extract<EntityMCostumeDisplayCoordinateAdjustment>(header, db, options),
EntityMCostumeDisplaySwitch = Extract<EntityMCostumeDisplaySwitch>(header, db, options),
EntityMCostumeDuplicationExchangePossessionGroup = Extract<EntityMCostumeDuplicationExchangePossessionGroup>(header, db, options),
EntityMCostumeEmblem = Extract<EntityMCostumeEmblem>(header, db, options),
EntityMCostumeEnhanced = Extract<EntityMCostumeEnhanced>(header, db, options),
EntityMCostumeLevelBonus = Extract<EntityMCostumeLevelBonus>(header, db, options),
EntityMCostumeLimitBreakMaterialGroup = Extract<EntityMCostumeLimitBreakMaterialGroup>(header, db, options),
EntityMCostumeLimitBreakMaterialRarityGroup = Extract<EntityMCostumeLimitBreakMaterialRarityGroup>(header, db, options),
EntityMCostumeLotteryEffectMaterialGroup = Extract<EntityMCostumeLotteryEffectMaterialGroup>(header, db, options),
EntityMCostumeLotteryEffectOddsGroup = Extract<EntityMCostumeLotteryEffectOddsGroup>(header, db, options),
EntityMCostumeLotteryEffectReleaseSchedule = Extract<EntityMCostumeLotteryEffectReleaseSchedule>(header, db, options),
EntityMCostumeLotteryEffect = Extract<EntityMCostumeLotteryEffect>(header, db, options),
EntityMCostumeLotteryEffectTargetAbility = Extract<EntityMCostumeLotteryEffectTargetAbility>(header, db, options),
EntityMCostumeLotteryEffectTargetStatusUp = Extract<EntityMCostumeLotteryEffectTargetStatusUp>(header, db, options),
EntityMCostumeOverflowExchangePossessionGroup = Extract<EntityMCostumeOverflowExchangePossessionGroup>(header, db, options),
EntityMCostumeProperAttributeHpBonus = Extract<EntityMCostumeProperAttributeHpBonus>(header, db, options),
EntityMCostumeRarity = Extract<EntityMCostumeRarity>(header, db, options),
EntityMCostumeSpecialActActiveSkillConditionAttribute = Extract<EntityMCostumeSpecialActActiveSkillConditionAttribute>(header, db, options),
EntityMCostumeSpecialActActiveSkill = Extract<EntityMCostumeSpecialActActiveSkill>(header, db, options),
EntityMCostumeStatusCalculation = Extract<EntityMCostumeStatusCalculation>(header, db, options),
EntityMCostume = Extract<EntityMCostume>(header, db, options),
EntityMDeckEntrustCoefficientAttribute = Extract<EntityMDeckEntrustCoefficientAttribute>(header, db, options),
EntityMDeckEntrustCoefficientPartsSeriesBonusCount = Extract<EntityMDeckEntrustCoefficientPartsSeriesBonusCount>(header, db, options),
EntityMDeckEntrustCoefficientStatus = Extract<EntityMDeckEntrustCoefficientStatus>(header, db, options),
EntityMDokanContentGroup = Extract<EntityMDokanContentGroup>(header, db, options),
EntityMDokan = Extract<EntityMDokan>(header, db, options),
EntityMDokanText = Extract<EntityMDokanText>(header, db, options),
EntityMEnhanceCampaign = Extract<EntityMEnhanceCampaign>(header, db, options),
EntityMEnhanceCampaignTargetGroup = Extract<EntityMEnhanceCampaignTargetGroup>(header, db, options),
EntityMEvaluateCondition = Extract<EntityMEvaluateCondition>(header, db, options),
EntityMEvaluateConditionValueGroup = Extract<EntityMEvaluateConditionValueGroup>(header, db, options),
EntityMEventQuestChapterCharacter = Extract<EntityMEventQuestChapterCharacter>(header, db, options),
EntityMEventQuestChapterDifficultyLimitContentUnlock = Extract<EntityMEventQuestChapterDifficultyLimitContentUnlock>(header, db, options),
EntityMEventQuestChapterLimitContentRelation = Extract<EntityMEventQuestChapterLimitContentRelation>(header, db, options),
EntityMEventQuestChapter = Extract<EntityMEventQuestChapter>(header, db, options),
EntityMEventQuestDailyGroupCompleteReward = Extract<EntityMEventQuestDailyGroupCompleteReward>(header, db, options),
EntityMEventQuestDailyGroupMessage = Extract<EntityMEventQuestDailyGroupMessage>(header, db, options),
EntityMEventQuestDailyGroup = Extract<EntityMEventQuestDailyGroup>(header, db, options),
EntityMEventQuestDailyGroupTargetChapter = Extract<EntityMEventQuestDailyGroupTargetChapter>(header, db, options),
EntityMEventQuestDisplayItemGroup = Extract<EntityMEventQuestDisplayItemGroup>(header, db, options),
EntityMEventQuestGuerrillaFreeOpenScheduleCorrespondence = Extract<EntityMEventQuestGuerrillaFreeOpenScheduleCorrespondence>(header, db, options),
EntityMEventQuestGuerrillaFreeOpen = Extract<EntityMEventQuestGuerrillaFreeOpen>(header, db, options),
EntityMEventQuestLabyrinthMob = Extract<EntityMEventQuestLabyrinthMob>(header, db, options),
EntityMEventQuestLabyrinthQuestDisplay = Extract<EntityMEventQuestLabyrinthQuestDisplay>(header, db, options),
EntityMEventQuestLabyrinthQuestEffectDescriptionAbility = Extract<EntityMEventQuestLabyrinthQuestEffectDescriptionAbility>(header, db, options),
EntityMEventQuestLabyrinthQuestEffectDescriptionFree = Extract<EntityMEventQuestLabyrinthQuestEffectDescriptionFree>(header, db, options),
EntityMEventQuestLabyrinthQuestEffectDisplay = Extract<EntityMEventQuestLabyrinthQuestEffectDisplay>(header, db, options),
EntityMEventQuestLabyrinthRewardGroup = Extract<EntityMEventQuestLabyrinthRewardGroup>(header, db, options),
EntityMEventQuestLabyrinthSeasonRewardGroup = Extract<EntityMEventQuestLabyrinthSeasonRewardGroup>(header, db, options),
EntityMEventQuestLabyrinthSeason = Extract<EntityMEventQuestLabyrinthSeason>(header, db, options),
EntityMEventQuestLabyrinthStageAccumulationRewardGroup = Extract<EntityMEventQuestLabyrinthStageAccumulationRewardGroup>(header, db, options),
EntityMEventQuestLabyrinthStage = Extract<EntityMEventQuestLabyrinthStage>(header, db, options),
EntityMEventQuestLimitContentDeckRestriction = Extract<EntityMEventQuestLimitContentDeckRestriction>(header, db, options),
EntityMEventQuestLimitContentDeckRestrictionTarget = Extract<EntityMEventQuestLimitContentDeckRestrictionTarget>(header, db, options),
EntityMEventQuestLimitContent = Extract<EntityMEventQuestLimitContent>(header, db, options),
EntityMEventQuestLink = Extract<EntityMEventQuestLink>(header, db, options),
EntityMEventQuestSequenceGroup = Extract<EntityMEventQuestSequenceGroup>(header, db, options),
EntityMEventQuestSequence = Extract<EntityMEventQuestSequence>(header, db, options),
EntityMEventQuestTowerAccumulationRewardGroup = Extract<EntityMEventQuestTowerAccumulationRewardGroup>(header, db, options),
EntityMEventQuestTowerAccumulationReward = Extract<EntityMEventQuestTowerAccumulationReward>(header, db, options),
EntityMEventQuestTowerAsset = Extract<EntityMEventQuestTowerAsset>(header, db, options),
EntityMEventQuestTowerRewardGroup = Extract<EntityMEventQuestTowerRewardGroup>(header, db, options),
EntityMEventQuestUnlockCondition = Extract<EntityMEventQuestUnlockCondition>(header, db, options),
EntityMExploreGradeAsset = Extract<EntityMExploreGradeAsset>(header, db, options),
EntityMExploreGradeScore = Extract<EntityMExploreGradeScore>(header, db, options),
EntityMExploreGroup = Extract<EntityMExploreGroup>(header, db, options),
EntityMExplore = Extract<EntityMExplore>(header, db, options),
EntityMExploreUnlockCondition = Extract<EntityMExploreUnlockCondition>(header, db, options),
EntityMExtraQuestGroupInMainQuestChapter = Extract<EntityMExtraQuestGroupInMainQuestChapter>(header, db, options),
EntityMExtraQuestGroup = Extract<EntityMExtraQuestGroup>(header, db, options),
EntityMFieldEffectBlessRelation = Extract<EntityMFieldEffectBlessRelation>(header, db, options),
EntityMFieldEffectDecreasePoint = Extract<EntityMFieldEffectDecreasePoint>(header, db, options),
EntityMFieldEffectGroup = Extract<EntityMFieldEffectGroup>(header, db, options),
EntityMGachaMedal = Extract<EntityMGachaMedal>(header, db, options),
EntityMGiftText = Extract<EntityMGiftText>(header, db, options),
EntityMGimmickAdditionalAsset = Extract<EntityMGimmickAdditionalAsset>(header, db, options),
EntityMGimmickExtraQuest = Extract<EntityMGimmickExtraQuest>(header, db, options),
EntityMGimmickGroupEventLog = Extract<EntityMGimmickGroupEventLog>(header, db, options),
EntityMGimmickGroup = Extract<EntityMGimmickGroup>(header, db, options),
EntityMGimmickInterval = Extract<EntityMGimmickInterval>(header, db, options),
EntityMGimmickOrnament = Extract<EntityMGimmickOrnament>(header, db, options),
EntityMGimmickSequenceGroup = Extract<EntityMGimmickSequenceGroup>(header, db, options),
EntityMGimmickSequenceRewardGroup = Extract<EntityMGimmickSequenceRewardGroup>(header, db, options),
EntityMGimmickSequenceSchedule = Extract<EntityMGimmickSequenceSchedule>(header, db, options),
EntityMGimmickSequence = Extract<EntityMGimmickSequence>(header, db, options),
EntityMGimmick = Extract<EntityMGimmick>(header, db, options),
EntityMHeadupDisplayView = Extract<EntityMHeadupDisplayView>(header, db, options),
EntityMHelpCategory = Extract<EntityMHelpCategory>(header, db, options),
EntityMHelpItem = Extract<EntityMHelpItem>(header, db, options),
EntityMHelpPageGroup = Extract<EntityMHelpPageGroup>(header, db, options),
EntityMHelp = Extract<EntityMHelp>(header, db, options),
EntityMImportantItemEffectDropCount = Extract<EntityMImportantItemEffectDropCount>(header, db, options),
EntityMImportantItemEffectDropRate = Extract<EntityMImportantItemEffectDropRate>(header, db, options),
EntityMImportantItemEffect = Extract<EntityMImportantItemEffect>(header, db, options),
EntityMImportantItemEffectTargetItemGroup = Extract<EntityMImportantItemEffectTargetItemGroup>(header, db, options),
EntityMImportantItemEffectTargetQuestGroup = Extract<EntityMImportantItemEffectTargetQuestGroup>(header, db, options),
EntityMImportantItemEffectUnlockFunction = Extract<EntityMImportantItemEffectUnlockFunction>(header, db, options),
EntityMImportantItem = Extract<EntityMImportantItem>(header, db, options),
EntityMLibraryEventQuestStoryGrouping = Extract<EntityMLibraryEventQuestStoryGrouping>(header, db, options),
EntityMLibraryMainQuestGroup = Extract<EntityMLibraryMainQuestGroup>(header, db, options),
EntityMLibraryMainQuestStory = Extract<EntityMLibraryMainQuestStory>(header, db, options),
EntityMLibraryMovieCategory = Extract<EntityMLibraryMovieCategory>(header, db, options),
EntityMLibraryMovie = Extract<EntityMLibraryMovie>(header, db, options),
EntityMLibraryMovieUnlockCondition = Extract<EntityMLibraryMovieUnlockCondition>(header, db, options),
EntityMLibraryRecordGrouping = Extract<EntityMLibraryRecordGrouping>(header, db, options),
EntityMLibraryStoryGroup = Extract<EntityMLibraryStoryGroup>(header, db, options),
EntityMLimitedOpenTextGroup = Extract<EntityMLimitedOpenTextGroup>(header, db, options),
EntityMLimitedOpenText = Extract<EntityMLimitedOpenText>(header, db, options),
EntityMListSettingAbilityGroup = Extract<EntityMListSettingAbilityGroup>(header, db, options),
EntityMListSettingAbilityGroupTarget = Extract<EntityMListSettingAbilityGroupTarget>(header, db, options),
EntityMLoginBonusStamp = Extract<EntityMLoginBonusStamp>(header, db, options),
EntityMLoginBonus = Extract<EntityMLoginBonus>(header, db, options),
EntityMMainQuestChapter = Extract<EntityMMainQuestChapter>(header, db, options),
EntityMMainQuestPortalCageCharacter = Extract<EntityMMainQuestPortalCageCharacter>(header, db, options),
EntityMMainQuestRouteAnotherReplayFlowUnlockCondition = Extract<EntityMMainQuestRouteAnotherReplayFlowUnlockCondition>(header, db, options),
EntityMMainQuestRoute = Extract<EntityMMainQuestRoute>(header, db, options),
EntityMMainQuestSeason = Extract<EntityMMainQuestSeason>(header, db, options),
EntityMMainQuestSequenceGroup = Extract<EntityMMainQuestSequenceGroup>(header, db, options),
EntityMMainQuestSequence = Extract<EntityMMainQuestSequence>(header, db, options),
EntityMMaintenanceGroup = Extract<EntityMMaintenanceGroup>(header, db, options),
EntityMMaintenance = Extract<EntityMMaintenance>(header, db, options),
EntityMMaterialSaleObtainPossession = Extract<EntityMMaterialSaleObtainPossession>(header, db, options),
EntityMMaterial = Extract<EntityMMaterial>(header, db, options),
EntityMMissionClearConditionValueView = Extract<EntityMMissionClearConditionValueView>(header, db, options),
EntityMMissionGroup = Extract<EntityMMissionGroup>(header, db, options),
EntityMMissionLink = Extract<EntityMMissionLink>(header, db, options),
EntityMMissionPassLevelGroup = Extract<EntityMMissionPassLevelGroup>(header, db, options),
EntityMMissionPassMissionGroup = Extract<EntityMMissionPassMissionGroup>(header, db, options),
EntityMMissionPassRewardGroup = Extract<EntityMMissionPassRewardGroup>(header, db, options),
EntityMMissionPass = Extract<EntityMMissionPass>(header, db, options),
EntityMMissionReward = Extract<EntityMMissionReward>(header, db, options),
EntityMMissionSubCategoryText = Extract<EntityMMissionSubCategoryText>(header, db, options),
EntityMMission = Extract<EntityMMission>(header, db, options),
EntityMMissionTerm = Extract<EntityMMissionTerm>(header, db, options),
EntityMMissionUnlockCondition = Extract<EntityMMissionUnlockCondition>(header, db, options),
EntityMMomBanner = Extract<EntityMMomBanner>(header, db, options),
EntityMMomPointBanner = Extract<EntityMMomPointBanner>(header, db, options),
EntityMMovie = Extract<EntityMMovie>(header, db, options),
EntityMNaviCutInContentGroup = Extract<EntityMNaviCutInContentGroup>(header, db, options),
EntityMNaviCutIn = Extract<EntityMNaviCutIn>(header, db, options),
EntityMNaviCutInText = Extract<EntityMNaviCutInText>(header, db, options),
EntityMNumericalFunctionParameterGroup = Extract<EntityMNumericalFunctionParameterGroup>(header, db, options),
EntityMNumericalFunction = Extract<EntityMNumericalFunction>(header, db, options),
EntityMNumericalParameterMap = Extract<EntityMNumericalParameterMap>(header, db, options),
EntityMOmikuji = Extract<EntityMOmikuji>(header, db, options),
EntityMOverrideHitEffectConditionCritical = Extract<EntityMOverrideHitEffectConditionCritical>(header, db, options),
EntityMOverrideHitEffectConditionDamageAttribute = Extract<EntityMOverrideHitEffectConditionDamageAttribute>(header, db, options),
EntityMOverrideHitEffectConditionGroup = Extract<EntityMOverrideHitEffectConditionGroup>(header, db, options),
EntityMOverrideHitEffectConditionSkillExecutor = Extract<EntityMOverrideHitEffectConditionSkillExecutor>(header, db, options),
EntityMPartsEnhancedSubStatus = Extract<EntityMPartsEnhancedSubStatus>(header, db, options),
EntityMPartsEnhanced = Extract<EntityMPartsEnhanced>(header, db, options),
EntityMPartsGroup = Extract<EntityMPartsGroup>(header, db, options),
EntityMPartsLevelUpPriceGroup = Extract<EntityMPartsLevelUpPriceGroup>(header, db, options),
EntityMPartsLevelUpRateGroup = Extract<EntityMPartsLevelUpRateGroup>(header, db, options),
EntityMPartsRarity = Extract<EntityMPartsRarity>(header, db, options),
EntityMPartsSeriesBonusAbilityGroup = Extract<EntityMPartsSeriesBonusAbilityGroup>(header, db, options),
EntityMPartsSeries = Extract<EntityMPartsSeries>(header, db, options),
EntityMPartsStatusMain = Extract<EntityMPartsStatusMain>(header, db, options),
EntityMParts = Extract<EntityMParts>(header, db, options),
EntityMPlatformPaymentPrice = Extract<EntityMPlatformPaymentPrice>(header, db, options),
EntityMPlatformPayment = Extract<EntityMPlatformPayment>(header, db, options),
EntityMPortalCageAccessPointFunctionGroupSchedule = Extract<EntityMPortalCageAccessPointFunctionGroupSchedule>(header, db, options),
EntityMPortalCageAccessPointFunctionGroup = Extract<EntityMPortalCageAccessPointFunctionGroup>(header, db, options),
EntityMPortalCageCharacterGroup = Extract<EntityMPortalCageCharacterGroup>(header, db, options),
EntityMPortalCageGate = Extract<EntityMPortalCageGate>(header, db, options),
EntityMPortalCageScene = Extract<EntityMPortalCageScene>(header, db, options),
EntityMPossessionAcquisitionRoute = Extract<EntityMPossessionAcquisitionRoute>(header, db, options),
EntityMPowerCalculationConstantValue = Extract<EntityMPowerCalculationConstantValue>(header, db, options),
EntityMPowerReferenceStatusGroup = Extract<EntityMPowerReferenceStatusGroup>(header, db, options),
EntityMPremiumItem = Extract<EntityMPremiumItem>(header, db, options),
EntityMPvpBackground = Extract<EntityMPvpBackground>(header, db, options),
EntityMPvpGradeGroup = Extract<EntityMPvpGradeGroup>(header, db, options),
EntityMPvpGradeOneMatchRewardGroup = Extract<EntityMPvpGradeOneMatchRewardGroup>(header, db, options),
EntityMPvpGradeOneMatchReward = Extract<EntityMPvpGradeOneMatchReward>(header, db, options),
EntityMPvpGrade = Extract<EntityMPvpGrade>(header, db, options),
EntityMPvpGradeWeeklyRewardGroup = Extract<EntityMPvpGradeWeeklyRewardGroup>(header, db, options),
EntityMPvpReward = Extract<EntityMPvpReward>(header, db, options),
EntityMPvpSeasonGrade = Extract<EntityMPvpSeasonGrade>(header, db, options),
EntityMPvpSeasonGrouping = Extract<EntityMPvpSeasonGrouping>(header, db, options),
EntityMPvpSeasonRankRewardGroup = Extract<EntityMPvpSeasonRankRewardGroup>(header, db, options),
EntityMPvpSeasonRankRewardPerSeason = Extract<EntityMPvpSeasonRankRewardPerSeason>(header, db, options),
EntityMPvpSeasonRankRewardRankGroup = Extract<EntityMPvpSeasonRankRewardRankGroup>(header, db, options),
EntityMPvpSeasonRankReward = Extract<EntityMPvpSeasonRankReward>(header, db, options),
EntityMPvpSeason = Extract<EntityMPvpSeason>(header, db, options),
EntityMPvpWeeklyRankRewardGroup = Extract<EntityMPvpWeeklyRankRewardGroup>(header, db, options),
EntityMPvpWeeklyRankRewardRankGroup = Extract<EntityMPvpWeeklyRankRewardRankGroup>(header, db, options),
EntityMPvpWinStreakCountEffect = Extract<EntityMPvpWinStreakCountEffect>(header, db, options),
EntityMQuestBonusAbility = Extract<EntityMQuestBonusAbility>(header, db, options),
EntityMQuestBonusAllyCharacter = Extract<EntityMQuestBonusAllyCharacter>(header, db, options),
EntityMQuestBonusCharacterGroup = Extract<EntityMQuestBonusCharacterGroup>(header, db, options),
EntityMQuestBonusCostumeGroup = Extract<EntityMQuestBonusCostumeGroup>(header, db, options),
EntityMQuestBonusCostumeSettingGroup = Extract<EntityMQuestBonusCostumeSettingGroup>(header, db, options),
EntityMQuestBonusDropReward = Extract<EntityMQuestBonusDropReward>(header, db, options),
EntityMQuestBonusEffectGroup = Extract<EntityMQuestBonusEffectGroup>(header, db, options),
EntityMQuestBonusExp = Extract<EntityMQuestBonusExp>(header, db, options),
EntityMQuestBonus = Extract<EntityMQuestBonus>(header, db, options),
EntityMQuestBonusTermGroup = Extract<EntityMQuestBonusTermGroup>(header, db, options),
EntityMQuestBonusWeaponGroup = Extract<EntityMQuestBonusWeaponGroup>(header, db, options),
EntityMQuestCampaignEffectGroup = Extract<EntityMQuestCampaignEffectGroup>(header, db, options),
EntityMQuestCampaign = Extract<EntityMQuestCampaign>(header, db, options),
EntityMQuestCampaignTargetGroup = Extract<EntityMQuestCampaignTargetGroup>(header, db, options),
EntityMQuestCampaignTargetItemGroup = Extract<EntityMQuestCampaignTargetItemGroup>(header, db, options),
EntityMQuestDeckMultiRestrictionGroup = Extract<EntityMQuestDeckMultiRestrictionGroup>(header, db, options),
EntityMQuestDeckRestrictionGroup = Extract<EntityMQuestDeckRestrictionGroup>(header, db, options),
EntityMQuestDeckRestrictionGroupUnlock = Extract<EntityMQuestDeckRestrictionGroupUnlock>(header, db, options),
EntityMQuestDisplayAttributeGroup = Extract<EntityMQuestDisplayAttributeGroup>(header, db, options),
EntityMQuestDisplayEnemyThumbnailReplace = Extract<EntityMQuestDisplayEnemyThumbnailReplace>(header, db, options),
EntityMQuestFirstClearRewardGroup = Extract<EntityMQuestFirstClearRewardGroup>(header, db, options),
EntityMQuestFirstClearRewardSwitch = Extract<EntityMQuestFirstClearRewardSwitch>(header, db, options),
EntityMQuestMissionConditionValueGroup = Extract<EntityMQuestMissionConditionValueGroup>(header, db, options),
EntityMQuestMissionGroup = Extract<EntityMQuestMissionGroup>(header, db, options),
EntityMQuestMissionReward = Extract<EntityMQuestMissionReward>(header, db, options),
EntityMQuestMission = Extract<EntityMQuestMission>(header, db, options),
EntityMQuestPickupRewardGroup = Extract<EntityMQuestPickupRewardGroup>(header, db, options),
EntityMQuestRelationMainFlow = Extract<EntityMQuestRelationMainFlow>(header, db, options),
EntityMQuestReleaseConditionBigHuntScore = Extract<EntityMQuestReleaseConditionBigHuntScore>(header, db, options),
EntityMQuestReleaseConditionCharacterLevel = Extract<EntityMQuestReleaseConditionCharacterLevel>(header, db, options),
EntityMQuestReleaseConditionDeckPower = Extract<EntityMQuestReleaseConditionDeckPower>(header, db, options),
EntityMQuestReleaseConditionGroup = Extract<EntityMQuestReleaseConditionGroup>(header, db, options),
EntityMQuestReleaseConditionList = Extract<EntityMQuestReleaseConditionList>(header, db, options),
EntityMQuestReleaseConditionQuestChallenge = Extract<EntityMQuestReleaseConditionQuestChallenge>(header, db, options),
EntityMQuestReleaseConditionQuestClear = Extract<EntityMQuestReleaseConditionQuestClear>(header, db, options),
EntityMQuestReleaseConditionUserLevel = Extract<EntityMQuestReleaseConditionUserLevel>(header, db, options),
EntityMQuestReleaseConditionWeaponAcquisition = Extract<EntityMQuestReleaseConditionWeaponAcquisition>(header, db, options),
EntityMQuestReplayFlowRewardGroup = Extract<EntityMQuestReplayFlowRewardGroup>(header, db, options),
EntityMQuestSceneBattle = Extract<EntityMQuestSceneBattle>(header, db, options),
EntityMQuestSceneChoiceCostumeEffectGroup = Extract<EntityMQuestSceneChoiceCostumeEffectGroup>(header, db, options),
EntityMQuestSceneChoiceEffect = Extract<EntityMQuestSceneChoiceEffect>(header, db, options),
EntityMQuestSceneChoice = Extract<EntityMQuestSceneChoice>(header, db, options),
EntityMQuestSceneChoiceWeaponEffectGroup = Extract<EntityMQuestSceneChoiceWeaponEffectGroup>(header, db, options),
EntityMQuestSceneNotConfirmTitleDialog = Extract<EntityMQuestSceneNotConfirmTitleDialog>(header, db, options),
EntityMQuestSceneOutgameBlendshapeMotion = Extract<EntityMQuestSceneOutgameBlendshapeMotion>(header, db, options),
EntityMQuestScenePictureBookReplace = Extract<EntityMQuestScenePictureBookReplace>(header, db, options),
EntityMQuestScene = Extract<EntityMQuestScene>(header, db, options),
EntityMQuestScheduleCorrespondence = Extract<EntityMQuestScheduleCorrespondence>(header, db, options),
EntityMQuestSchedule = Extract<EntityMQuestSchedule>(header, db, options),
EntityMQuest = Extract<EntityMQuest>(header, db, options),
EntityMReport = Extract<EntityMReport>(header, db, options),
EntityMShopDisplayPrice = Extract<EntityMShopDisplayPrice>(header, db, options),
EntityMShopItemAdditionalContent = Extract<EntityMShopItemAdditionalContent>(header, db, options),
EntityMShopItemCellGroup = Extract<EntityMShopItemCellGroup>(header, db, options),
EntityMShopItemCellLimitedOpen = Extract<EntityMShopItemCellLimitedOpen>(header, db, options),
EntityMShopItemCell = Extract<EntityMShopItemCell>(header, db, options),
EntityMShopItemCellTerm = Extract<EntityMShopItemCellTerm>(header, db, options),
EntityMShopItemContentEffect = Extract<EntityMShopItemContentEffect>(header, db, options),
EntityMShopItemContentMission = Extract<EntityMShopItemContentMission>(header, db, options),
EntityMShopItemContentPossession = Extract<EntityMShopItemContentPossession>(header, db, options),
EntityMShopItemLimitedStock = Extract<EntityMShopItemLimitedStock>(header, db, options),
EntityMShopItem = Extract<EntityMShopItem>(header, db, options),
EntityMShopItemUserLevelCondition = Extract<EntityMShopItemUserLevelCondition>(header, db, options),
EntityMShopReplaceableGem = Extract<EntityMShopReplaceableGem>(header, db, options),
EntityMShop = Extract<EntityMShop>(header, db, options),
EntityMSideStoryQuestLimitContent = Extract<EntityMSideStoryQuestLimitContent>(header, db, options),
EntityMSideStoryQuestScene = Extract<EntityMSideStoryQuestScene>(header, db, options),
EntityMSideStoryQuest = Extract<EntityMSideStoryQuest>(header, db, options),
EntityMSkillAbnormalBehaviourActionAbnormalResistance = Extract<EntityMSkillAbnormalBehaviourActionAbnormalResistance>(header, db, options),
EntityMSkillAbnormalBehaviourActionAttributeDamageCorrection = Extract<EntityMSkillAbnormalBehaviourActionAttributeDamageCorrection>(header, db, options),
EntityMSkillAbnormalBehaviourActionBuffResistance = Extract<EntityMSkillAbnormalBehaviourActionBuffResistance>(header, db, options),
EntityMSkillAbnormalBehaviourActionDamageMultiplyDetailAlways = Extract<EntityMSkillAbnormalBehaviourActionDamageMultiplyDetailAlways>(header, db, options),
EntityMSkillAbnormalBehaviourActionDamageMultiply = Extract<EntityMSkillAbnormalBehaviourActionDamageMultiply>(header, db, options),
EntityMSkillAbnormalBehaviourActionDamage = Extract<EntityMSkillAbnormalBehaviourActionDamage>(header, db, options),
EntityMSkillAbnormalBehaviourActionDefaultSkillLottery = Extract<EntityMSkillAbnormalBehaviourActionDefaultSkillLottery>(header, db, options),
EntityMSkillAbnormalBehaviourActionHitRatioDown = Extract<EntityMSkillAbnormalBehaviourActionHitRatioDown>(header, db, options),
EntityMSkillAbnormalBehaviourActionModifyHateValue = Extract<EntityMSkillAbnormalBehaviourActionModifyHateValue>(header, db, options),
EntityMSkillAbnormalBehaviourActionOverrideEvasionValue = Extract<EntityMSkillAbnormalBehaviourActionOverrideEvasionValue>(header, db, options),
EntityMSkillAbnormalBehaviourActionOverrideHitEffect = Extract<EntityMSkillAbnormalBehaviourActionOverrideHitEffect>(header, db, options),
EntityMSkillAbnormalBehaviourActionRecovery = Extract<EntityMSkillAbnormalBehaviourActionRecovery>(header, db, options),
EntityMSkillAbnormalBehaviourActionTurnRestriction = Extract<EntityMSkillAbnormalBehaviourActionTurnRestriction>(header, db, options),
EntityMSkillAbnormalBehaviourGroup = Extract<EntityMSkillAbnormalBehaviourGroup>(header, db, options),
EntityMSkillAbnormalBehaviour = Extract<EntityMSkillAbnormalBehaviour>(header, db, options),
EntityMSkillAbnormalDamageMultiplyDetailAbnormal = Extract<EntityMSkillAbnormalDamageMultiplyDetailAbnormal>(header, db, options),
EntityMSkillAbnormalDamageMultiplyDetailBuffAttached = Extract<EntityMSkillAbnormalDamageMultiplyDetailBuffAttached>(header, db, options),
EntityMSkillAbnormalDamageMultiplyDetailCritical = Extract<EntityMSkillAbnormalDamageMultiplyDetailCritical>(header, db, options),
EntityMSkillAbnormalDamageMultiplyDetailHitIndex = Extract<EntityMSkillAbnormalDamageMultiplyDetailHitIndex>(header, db, options),
EntityMSkillAbnormalDamageMultiplyDetailSkillfulWeapon = Extract<EntityMSkillAbnormalDamageMultiplyDetailSkillfulWeapon>(header, db, options),
EntityMSkillAbnormalLifetimeBehaviourActivateCount = Extract<EntityMSkillAbnormalLifetimeBehaviourActivateCount>(header, db, options),
EntityMSkillAbnormalLifetimeBehaviourFrameCount = Extract<EntityMSkillAbnormalLifetimeBehaviourFrameCount>(header, db, options),
EntityMSkillAbnormalLifetimeBehaviourGroup = Extract<EntityMSkillAbnormalLifetimeBehaviourGroup>(header, db, options),
EntityMSkillAbnormalLifetimeBehaviourReceiveDamageCount = Extract<EntityMSkillAbnormalLifetimeBehaviourReceiveDamageCount>(header, db, options),
EntityMSkillAbnormalLifetimeBehaviourTurnCount = Extract<EntityMSkillAbnormalLifetimeBehaviourTurnCount>(header, db, options),
EntityMSkillAbnormalLifetime = Extract<EntityMSkillAbnormalLifetime>(header, db, options),
EntityMSkillAbnormal = Extract<EntityMSkillAbnormal>(header, db, options),
EntityMSkillBehaviourActionAbnormal = Extract<EntityMSkillBehaviourActionAbnormal>(header, db, options),
EntityMSkillBehaviourActionActiveSkillDamageCorrection = Extract<EntityMSkillBehaviourActionActiveSkillDamageCorrection>(header, db, options),
EntityMSkillBehaviourActionAdvanceActiveSkillCooltimeImmediate = Extract<EntityMSkillBehaviourActionAdvanceActiveSkillCooltimeImmediate>(header, db, options),
EntityMSkillBehaviourActionAdvanceActiveSkillCooltime = Extract<EntityMSkillBehaviourActionAdvanceActiveSkillCooltime>(header, db, options),
EntityMSkillBehaviourActionAttackClampHp = Extract<EntityMSkillBehaviourActionAttackClampHp>(header, db, options),
EntityMSkillBehaviourActionAttackCombo = Extract<EntityMSkillBehaviourActionAttackCombo>(header, db, options),
EntityMSkillBehaviourActionAttackFixedDamage = Extract<EntityMSkillBehaviourActionAttackFixedDamage>(header, db, options),
EntityMSkillBehaviourActionAttackHpRatio = Extract<EntityMSkillBehaviourActionAttackHpRatio>(header, db, options),
EntityMSkillBehaviourActionAttackIgnoreVitality = Extract<EntityMSkillBehaviourActionAttackIgnoreVitality>(header, db, options),
EntityMSkillBehaviourActionAttackMainWeaponAttribute = Extract<EntityMSkillBehaviourActionAttackMainWeaponAttribute>(header, db, options),
EntityMSkillBehaviourActionAttackSkillfulMainWeaponType = Extract<EntityMSkillBehaviourActionAttackSkillfulMainWeaponType>(header, db, options),
EntityMSkillBehaviourActionAttack = Extract<EntityMSkillBehaviourActionAttack>(header, db, options),
EntityMSkillBehaviourActionAttackVitality = Extract<EntityMSkillBehaviourActionAttackVitality>(header, db, options),
EntityMSkillBehaviourActionAttributeDamageCorrection = Extract<EntityMSkillBehaviourActionAttributeDamageCorrection>(header, db, options),
EntityMSkillBehaviourActionBuff = Extract<EntityMSkillBehaviourActionBuff>(header, db, options),
EntityMSkillBehaviourActionChangestep = Extract<EntityMSkillBehaviourActionChangestep>(header, db, options),
EntityMSkillBehaviourActionDamageCorrectionHpRatio = Extract<EntityMSkillBehaviourActionDamageCorrectionHpRatio>(header, db, options),
EntityMSkillBehaviourActionDamageMultiply = Extract<EntityMSkillBehaviourActionDamageMultiply>(header, db, options),
EntityMSkillBehaviourActionDefaultSkillLottery = Extract<EntityMSkillBehaviourActionDefaultSkillLottery>(header, db, options),
EntityMSkillBehaviourActionExtendBuffCooltime = Extract<EntityMSkillBehaviourActionExtendBuffCooltime>(header, db, options),
EntityMSkillBehaviourActionHpRatioDamage = Extract<EntityMSkillBehaviourActionHpRatioDamage>(header, db, options),
EntityMSkillBehaviourActionOverlimitDamageMultiply = Extract<EntityMSkillBehaviourActionOverlimitDamageMultiply>(header, db, options),
EntityMSkillBehaviourActionRecoveryPointCorrection = Extract<EntityMSkillBehaviourActionRecoveryPointCorrection>(header, db, options),
EntityMSkillBehaviourActionRecovery = Extract<EntityMSkillBehaviourActionRecovery>(header, db, options),
EntityMSkillBehaviourActionRemoveAbnormal = Extract<EntityMSkillBehaviourActionRemoveAbnormal>(header, db, options),
EntityMSkillBehaviourActionRemoveBuff = Extract<EntityMSkillBehaviourActionRemoveBuff>(header, db, options),
EntityMSkillBehaviourActionShortenActiveSkillCooltime = Extract<EntityMSkillBehaviourActionShortenActiveSkillCooltime>(header, db, options),
EntityMSkillBehaviourActionSkillRecoveryPowerCorrection = Extract<EntityMSkillBehaviourActionSkillRecoveryPowerCorrection>(header, db, options),
EntityMSkillBehaviourActivationConditionActivationUpperCount = Extract<EntityMSkillBehaviourActivationConditionActivationUpperCount>(header, db, options),
EntityMSkillBehaviourActivationConditionAttribute = Extract<EntityMSkillBehaviourActivationConditionAttribute>(header, db, options),
EntityMSkillBehaviourActivationConditionGroup = Extract<EntityMSkillBehaviourActivationConditionGroup>(header, db, options),
EntityMSkillBehaviourActivationConditionHpRatio = Extract<EntityMSkillBehaviourActivationConditionHpRatio>(header, db, options),
EntityMSkillBehaviourActivationConditionInSkillFlow = Extract<EntityMSkillBehaviourActivationConditionInSkillFlow>(header, db, options),
EntityMSkillBehaviourActivationConditionWaveNumber = Extract<EntityMSkillBehaviourActivationConditionWaveNumber>(header, db, options),
EntityMSkillBehaviourActivationMethod = Extract<EntityMSkillBehaviourActivationMethod>(header, db, options),
EntityMSkillBehaviourGroup = Extract<EntityMSkillBehaviourGroup>(header, db, options),
EntityMSkillBehaviour = Extract<EntityMSkillBehaviour>(header, db, options),
EntityMSkillBuff = Extract<EntityMSkillBuff>(header, db, options),
EntityMSkillCasttimeBehaviourActionOnFrameUpdate = Extract<EntityMSkillCasttimeBehaviourActionOnFrameUpdate>(header, db, options),
EntityMSkillCasttimeBehaviourActionOnSkillDamageCondition = Extract<EntityMSkillCasttimeBehaviourActionOnSkillDamageCondition>(header, db, options),
EntityMSkillCasttimeBehaviourGroup = Extract<EntityMSkillCasttimeBehaviourGroup>(header, db, options),
EntityMSkillCasttimeBehaviour = Extract<EntityMSkillCasttimeBehaviour>(header, db, options),
EntityMSkillCasttime = Extract<EntityMSkillCasttime>(header, db, options),
EntityMSkillCooltimeAdvanceValueOnDefaultSkillGroup = Extract<EntityMSkillCooltimeAdvanceValueOnDefaultSkillGroup>(header, db, options),
EntityMSkillCooltimeBehaviourGroup = Extract<EntityMSkillCooltimeBehaviourGroup>(header, db, options),
EntityMSkillCooltimeBehaviourOnExecuteActiveSkill = Extract<EntityMSkillCooltimeBehaviourOnExecuteActiveSkill>(header, db, options),
EntityMSkillCooltimeBehaviourOnExecuteCompanionSkill = Extract<EntityMSkillCooltimeBehaviourOnExecuteCompanionSkill>(header, db, options),
EntityMSkillCooltimeBehaviourOnExecuteDefaultSkill = Extract<EntityMSkillCooltimeBehaviourOnExecuteDefaultSkill>(header, db, options),
EntityMSkillCooltimeBehaviourOnFrameUpdate = Extract<EntityMSkillCooltimeBehaviourOnFrameUpdate>(header, db, options),
EntityMSkillCooltimeBehaviourOnSkillDamage = Extract<EntityMSkillCooltimeBehaviourOnSkillDamage>(header, db, options),
EntityMSkillCooltimeBehaviour = Extract<EntityMSkillCooltimeBehaviour>(header, db, options),
EntityMSkillDamageMultiplyAbnormalAttachedValueGroup = Extract<EntityMSkillDamageMultiplyAbnormalAttachedValueGroup>(header, db, options),
EntityMSkillDamageMultiplyDetailAbnormalAttached = Extract<EntityMSkillDamageMultiplyDetailAbnormalAttached>(header, db, options),
EntityMSkillDamageMultiplyDetailAlways = Extract<EntityMSkillDamageMultiplyDetailAlways>(header, db, options),
EntityMSkillDamageMultiplyDetailBuffAttached = Extract<EntityMSkillDamageMultiplyDetailBuffAttached>(header, db, options),
EntityMSkillDamageMultiplyDetailCritical = Extract<EntityMSkillDamageMultiplyDetailCritical>(header, db, options),
EntityMSkillDamageMultiplyDetailHitIndex = Extract<EntityMSkillDamageMultiplyDetailHitIndex>(header, db, options),
EntityMSkillDamageMultiplyDetailSkillfulWeaponType = Extract<EntityMSkillDamageMultiplyDetailSkillfulWeaponType>(header, db, options),
EntityMSkillDamageMultiplyDetailSpecifiedCostumeType = Extract<EntityMSkillDamageMultiplyDetailSpecifiedCostumeType>(header, db, options),
EntityMSkillDamageMultiplyHitIndexValueGroup = Extract<EntityMSkillDamageMultiplyHitIndexValueGroup>(header, db, options),
EntityMSkillDamageMultiplyTargetSpecifiedCostumeGroup = Extract<EntityMSkillDamageMultiplyTargetSpecifiedCostumeGroup>(header, db, options),
EntityMSkillDetail = Extract<EntityMSkillDetail>(header, db, options),
EntityMSkillLevelGroup = Extract<EntityMSkillLevelGroup>(header, db, options),
EntityMSkillRemoveAbnormalTargetAbnormalGroup = Extract<EntityMSkillRemoveAbnormalTargetAbnormalGroup>(header, db, options),
EntityMSkillRemoveBuffFilterStatusKind = Extract<EntityMSkillRemoveBuffFilterStatusKind>(header, db, options),
EntityMSkillReserveUiType = Extract<EntityMSkillReserveUiType>(header, db, options),
EntityMSkill = Extract<EntityMSkill>(header, db, options),
EntityMSmartphoneChatGroupMessage = Extract<EntityMSmartphoneChatGroupMessage>(header, db, options),
EntityMSmartphoneChatGroup = Extract<EntityMSmartphoneChatGroup>(header, db, options),
EntityMSpeaker = Extract<EntityMSpeaker>(header, db, options),
EntityMStainedGlassStatusUpGroup = Extract<EntityMStainedGlassStatusUpGroup>(header, db, options),
EntityMStainedGlassStatusUpTargetGroup = Extract<EntityMStainedGlassStatusUpTargetGroup>(header, db, options),
EntityMStainedGlass = Extract<EntityMStainedGlass>(header, db, options),
EntityMThought = Extract<EntityMThought>(header, db, options),
EntityMTipBackgroundAsset = Extract<EntityMTipBackgroundAsset>(header, db, options),
EntityMTipDisplayConditionGroup = Extract<EntityMTipDisplayConditionGroup>(header, db, options),
EntityMTipGroupBackgroundAssetRelation = Extract<EntityMTipGroupBackgroundAssetRelation>(header, db, options),
EntityMTipGroupBackgroundAsset = Extract<EntityMTipGroupBackgroundAsset>(header, db, options),
EntityMTipGroupSelection = Extract<EntityMTipGroupSelection>(header, db, options),
EntityMTipGroupSituationSeason = Extract<EntityMTipGroupSituationSeason>(header, db, options),
EntityMTipGroupSituation = Extract<EntityMTipGroupSituation>(header, db, options),
EntityMTipGroup = Extract<EntityMTipGroup>(header, db, options),
EntityMTip = Extract<EntityMTip>(header, db, options),
EntityMTitleFlowMovie = Extract<EntityMTitleFlowMovie>(header, db, options),
EntityMTitleStillGroup = Extract<EntityMTitleStillGroup>(header, db, options),
EntityMTitleStill = Extract<EntityMTitleStill>(header, db, options),
EntityMTutorialConsumePossessionGroup = Extract<EntityMTutorialConsumePossessionGroup>(header, db, options),
EntityMTutorialDialog = Extract<EntityMTutorialDialog>(header, db, options),
EntityMTutorialUnlockCondition = Extract<EntityMTutorialUnlockCondition>(header, db, options),
EntityMUserLevel = Extract<EntityMUserLevel>(header, db, options),
EntityMUserQuestSceneGrantPossession = Extract<EntityMUserQuestSceneGrantPossession>(header, db, options),
EntityMWeaponAbilityEnhancementMaterial = Extract<EntityMWeaponAbilityEnhancementMaterial>(header, db, options),
EntityMWeaponAbilityGroup = Extract<EntityMWeaponAbilityGroup>(header, db, options),
EntityMWeaponAwakenAbility = Extract<EntityMWeaponAwakenAbility>(header, db, options),
EntityMWeaponAwakenEffectGroup = Extract<EntityMWeaponAwakenEffectGroup>(header, db, options),
EntityMWeaponAwakenMaterialGroup = Extract<EntityMWeaponAwakenMaterialGroup>(header, db, options),
EntityMWeaponAwakenStatusUpGroup = Extract<EntityMWeaponAwakenStatusUpGroup>(header, db, options),
EntityMWeaponAwaken = Extract<EntityMWeaponAwaken>(header, db, options),
EntityMWeaponBaseStatus = Extract<EntityMWeaponBaseStatus>(header, db, options),
EntityMWeaponConsumeExchangeConsumableItemGroup = Extract<EntityMWeaponConsumeExchangeConsumableItemGroup>(header, db, options),
EntityMWeaponEnhancedAbility = Extract<EntityMWeaponEnhancedAbility>(header, db, options),
EntityMWeaponEnhancedSkill = Extract<EntityMWeaponEnhancedSkill>(header, db, options),
EntityMWeaponEnhanced = Extract<EntityMWeaponEnhanced>(header, db, options),
EntityMWeaponEvolutionGroup = Extract<EntityMWeaponEvolutionGroup>(header, db, options),
EntityMWeaponEvolutionMaterialGroup = Extract<EntityMWeaponEvolutionMaterialGroup>(header, db, options),
EntityMWeaponFieldEffectDecreasePoint = Extract<EntityMWeaponFieldEffectDecreasePoint>(header, db, options),
EntityMWeaponRarityLimitBreakMaterialGroup = Extract<EntityMWeaponRarityLimitBreakMaterialGroup>(header, db, options),
EntityMWeaponRarity = Extract<EntityMWeaponRarity>(header, db, options),
EntityMWeaponSkillEnhancementMaterial = Extract<EntityMWeaponSkillEnhancementMaterial>(header, db, options),
EntityMWeaponSkillGroup = Extract<EntityMWeaponSkillGroup>(header, db, options),
EntityMWeaponSpecificEnhance = Extract<EntityMWeaponSpecificEnhance>(header, db, options),
EntityMWeaponSpecificLimitBreakMaterialGroup = Extract<EntityMWeaponSpecificLimitBreakMaterialGroup>(header, db, options),
EntityMWeaponStatusCalculation = Extract<EntityMWeaponStatusCalculation>(header, db, options),
EntityMWeaponStoryReleaseConditionGroup = Extract<EntityMWeaponStoryReleaseConditionGroup>(header, db, options),
EntityMWeaponStoryReleaseConditionOperationGroup = Extract<EntityMWeaponStoryReleaseConditionOperationGroup>(header, db, options),
EntityMWeaponStoryReleaseConditionOperation = Extract<EntityMWeaponStoryReleaseConditionOperation>(header, db, options),
EntityMWeapon = Extract<EntityMWeapon>(header, db, options),
EntityMWebviewMission = Extract<EntityMWebviewMission>(header, db, options),
EntityMWebviewMissionTitleText = Extract<EntityMWebviewMissionTitleText>(header, db, options),
EntityMWebviewPanelMissionCompleteFlavorText = Extract<EntityMWebviewPanelMissionCompleteFlavorText>(header, db, options),
EntityMWebviewPanelMissionPage = Extract<EntityMWebviewPanelMissionPage>(header, db, options),
EntityMWebviewPanelMission = Extract<EntityMWebviewPanelMission>(header, db, options)
};
}
/// <summary>
/// Extracts a single table from the binary data by looking up the table's offset in the header
/// and deserializing the MessagePack array at that position.
/// Returns an empty list if the table is not present in the header.
/// </summary>
private static List<T> Extract<T>(Dictionary<string, (int, int)> header, ReadOnlyMemory<byte> db, MessagePackSerializerOptions options) where T : class
{
var attr = typeof(T).GetCustomAttribute<MemoryTableAttribute>()
?? throw new InvalidOperationException($"{typeof(T).Name} is missing [MemoryTable] attribute.");
if (!header.TryGetValue(attr.TableName, out var pos))
return [];
var slice = db.Slice(pos.Item1, pos.Item2);
return MessagePackSerializer.Deserialize<T[]>(slice, options)?.ToList() ?? [];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,244 @@
using MariesWonderland.Models.Entities;
namespace MariesWonderland.Data;
/// <summary>
/// In-memory per-user database holding all client-visible (EntityI*) and server-only (EntityS*)
/// entity collections. One instance exists per logged-in user, keyed by userId in <see cref="UserDataStore"/>.
/// EntityI* properties are included in diff responses sent to the client; EntityS* properties are
/// server-side bookkeeping that is never transmitted.
/// </summary>
public class DarkUserMemoryDatabase
{
public List<EntityIUser> EntityIUser { get; set; } = [];
public List<EntityIUserApple> EntityIUserApple { get; set; } = [];
public List<EntityIUserAutoSaleSettingDetail> EntityIUserAutoSaleSettingDetail { get; set; } = [];
public List<EntityIUserBeginnerCampaign> EntityIUserBeginnerCampaign { get; set; } = [];
public List<EntityIUserBigHuntMaxScore> EntityIUserBigHuntMaxScore { get; set; } = [];
public List<EntityIUserBigHuntProgressStatus> EntityIUserBigHuntProgressStatus { get; set; } = [];
public List<EntityIUserBigHuntScheduleMaxScore> EntityIUserBigHuntScheduleMaxScore { get; set; } = [];
public List<EntityIUserBigHuntStatus> EntityIUserBigHuntStatus { get; set; } = [];
public List<EntityIUserBigHuntWeeklyMaxScore> EntityIUserBigHuntWeeklyMaxScore { get; set; } = [];
public List<EntityIUserBigHuntWeeklyStatus> EntityIUserBigHuntWeeklyStatus { get; set; } = [];
public List<EntityIUserCageOrnamentReward> EntityIUserCageOrnamentReward { get; set; } = [];
public List<EntityIUserCharacter> EntityIUserCharacter { get; set; } = [];
public List<EntityIUserCharacterBoard> EntityIUserCharacterBoard { get; set; } = [];
public List<EntityIUserCharacterBoardAbility> EntityIUserCharacterBoardAbility { get; set; } = [];
public List<EntityIUserCharacterBoardCompleteReward> EntityIUserCharacterBoardCompleteReward { get; set; } = [];
public List<EntityIUserCharacterBoardStatusUp> EntityIUserCharacterBoardStatusUp { get; set; } = [];
public List<EntityIUserCharacterCostumeLevelBonus> EntityIUserCharacterCostumeLevelBonus { get; set; } = [];
public List<EntityIUserCharacterRebirth> EntityIUserCharacterRebirth { get; set; } = [];
public List<EntityIUserCharacterViewerField> EntityIUserCharacterViewerField { get; set; } = [];
public List<EntityIUserComebackCampaign> EntityIUserComebackCampaign { get; set; } = [];
public List<EntityIUserCompanion> EntityIUserCompanion { get; set; } = [];
public List<EntityIUserConsumableItem> EntityIUserConsumableItem { get; set; } = [];
public List<EntityIUserContentsStory> EntityIUserContentsStory { get; set; } = [];
public List<EntityIUserCostume> EntityIUserCostume { get; set; } = [];
public List<EntityIUserCostumeActiveSkill> EntityIUserCostumeActiveSkill { get; set; } = [];
public List<EntityIUserCostumeAwakenStatusUp> EntityIUserCostumeAwakenStatusUp { get; set; } = [];
public List<EntityIUserCostumeLevelBonusReleaseStatus> EntityIUserCostumeLevelBonusReleaseStatus { get; set; } = [];
public List<EntityIUserCostumeLotteryEffect> EntityIUserCostumeLotteryEffect { get; set; } = [];
public List<EntityIUserCostumeLotteryEffectAbility> EntityIUserCostumeLotteryEffectAbility { get; set; } = [];
public List<EntityIUserCostumeLotteryEffectPending> EntityIUserCostumeLotteryEffectPending { get; set; } = [];
public List<EntityIUserCostumeLotteryEffectStatusUp> EntityIUserCostumeLotteryEffectStatusUp { get; set; } = [];
public List<EntityIUserDeck> EntityIUserDeck { get; set; } = [];
public List<EntityIUserDeckCharacter> EntityIUserDeckCharacter { get; set; } = [];
public List<EntityIUserDeckCharacterDressupCostume> EntityIUserDeckCharacterDressupCostume { get; set; } = [];
public List<EntityIUserDeckLimitContentDeletedCharacter> EntityIUserDeckLimitContentDeletedCharacter { get; set; } = [];
public List<EntityIUserDeckLimitContentRestricted> EntityIUserDeckLimitContentRestricted { get; set; } = [];
public List<EntityIUserDeckPartsGroup> EntityIUserDeckPartsGroup { get; set; } = [];
public List<EntityIUserDeckSubWeaponGroup> EntityIUserDeckSubWeaponGroup { get; set; } = [];
public List<EntityIUserDeckTypeNote> EntityIUserDeckTypeNote { get; set; } = [];
public List<EntityIUserDokan> EntityIUserDokan { get; set; } = [];
public List<EntityIUserEventQuestDailyGroupCompleteReward> EntityIUserEventQuestDailyGroupCompleteReward { get; set; } = [];
public List<EntityIUserEventQuestGuerrillaFreeOpen> EntityIUserEventQuestGuerrillaFreeOpen { get; set; } = [];
public List<EntityIUserEventQuestLabyrinthSeason> EntityIUserEventQuestLabyrinthSeason { get; set; } = [];
public List<EntityIUserEventQuestLabyrinthStage> EntityIUserEventQuestLabyrinthStage { get; set; } = [];
public List<EntityIUserEventQuestProgressStatus> EntityIUserEventQuestProgressStatus { get; set; } = [];
public List<EntityIUserEventQuestTowerAccumulationReward> EntityIUserEventQuestTowerAccumulationReward { get; set; } = [];
public List<EntityIUserExplore> EntityIUserExplore { get; set; } = [];
public List<EntityIUserExploreScore> EntityIUserExploreScore { get; set; } = [];
public List<EntityIUserExtraQuestProgressStatus> EntityIUserExtraQuestProgressStatus { get; set; } = [];
public List<EntityIUserFacebook> EntityIUserFacebook { get; set; } = [];
public List<EntityIUserGem> EntityIUserGem { get; set; } = [];
public List<EntitySUserGift> EntitySUserGift { get; set; } = [];
public List<EntityIUserGimmick> EntityIUserGimmick { get; set; } = [];
public List<EntityIUserGimmickOrnamentProgress> EntityIUserGimmickOrnamentProgress { get; set; } = [];
public List<EntityIUserGimmickSequence> EntityIUserGimmickSequence { get; set; } = [];
public List<EntityIUserGimmickUnlock> EntityIUserGimmickUnlock { get; set; } = [];
public List<EntityIUserImportantItem> EntityIUserImportantItem { get; set; } = [];
public List<EntityIUserLimitedOpen> EntityIUserLimitedOpen { get; set; } = [];
public List<EntityIUserLogin> EntityIUserLogin { get; set; } = [];
public List<EntityIUserLoginBonus> EntityIUserLoginBonus { get; set; } = [];
public List<EntityIUserMainQuestFlowStatus> EntityIUserMainQuestFlowStatus { get; set; } = [];
public List<EntityIUserMainQuestMainFlowStatus> EntityIUserMainQuestMainFlowStatus { get; set; } = [];
public List<EntityIUserMainQuestProgressStatus> EntityIUserMainQuestProgressStatus { get; set; } = [];
public List<EntityIUserMainQuestReplayFlowStatus> EntityIUserMainQuestReplayFlowStatus { get; set; } = [];
public List<EntityIUserMainQuestSeasonRoute> EntityIUserMainQuestSeasonRoute { get; set; } = [];
public List<EntityIUserMaterial> EntityIUserMaterial { get; set; } = [];
public List<EntityIUserMission> EntityIUserMission { get; set; } = [];
public List<EntityIUserMissionCompletionProgress> EntityIUserMissionCompletionProgress { get; set; } = [];
public List<EntityIUserMissionPassPoint> EntityIUserMissionPassPoint { get; set; } = [];
public List<EntityIUserMovie> EntityIUserMovie { get; set; } = [];
public List<EntityIUserNaviCutIn> EntityIUserNaviCutIn { get; set; } = [];
public List<EntityIUserOmikuji> EntityIUserOmikuji { get; set; } = [];
public List<EntityIUserParts> EntityIUserParts { get; set; } = [];
public List<EntityIUserPartsGroupNote> EntityIUserPartsGroupNote { get; set; } = [];
public List<EntityIUserPartsPreset> EntityIUserPartsPreset { get; set; } = [];
public List<EntityIUserPartsPresetTag> EntityIUserPartsPresetTag { get; set; } = [];
public List<EntityIUserPartsStatusSub> EntityIUserPartsStatusSub { get; set; } = [];
public List<EntityIUserPortalCageStatus> EntityIUserPortalCageStatus { get; set; } = [];
public List<EntityIUserPossessionAutoConvert> EntityIUserPossessionAutoConvert { get; set; } = [];
public List<EntityIUserPremiumItem> EntityIUserPremiumItem { get; set; } = [];
public List<EntityIUserProfile> EntityIUserProfile { get; set; } = [];
public List<EntityIUserPvpDefenseDeck> EntityIUserPvpDefenseDeck { get; set; } = [];
public List<EntityIUserPvpStatus> EntityIUserPvpStatus { get; set; } = [];
public List<EntityIUserPvpWeeklyResult> EntityIUserPvpWeeklyResult { get; set; } = [];
public List<EntityIUserQuest> EntityIUserQuest { get; set; } = [];
public List<EntityIUserQuestAutoOrbit> EntityIUserQuestAutoOrbit { get; set; } = [];
public List<EntityIUserQuestLimitContentStatus> EntityIUserQuestLimitContentStatus { get; set; } = [];
public List<EntityIUserQuestMission> EntityIUserQuestMission { get; set; } = [];
public List<EntityIUserQuestReplayFlowRewardGroup> EntityIUserQuestReplayFlowRewardGroup { get; set; } = [];
public List<EntityIUserQuestSceneChoice> EntityIUserQuestSceneChoice { get; set; } = [];
public List<EntityIUserQuestSceneChoiceHistory> EntityIUserQuestSceneChoiceHistory { get; set; } = [];
public List<EntityIUserSetting> EntityIUserSetting { get; set; } = [];
public List<EntityIUserShopItem> EntityIUserShopItem { get; set; } = [];
public List<EntityIUserShopReplaceable> EntityIUserShopReplaceable { get; set; } = [];
public List<EntityIUserShopReplaceableLineup> EntityIUserShopReplaceableLineup { get; set; } = [];
public List<EntityIUserSideStoryQuest> EntityIUserSideStoryQuest { get; set; } = [];
public List<EntityIUserSideStoryQuestSceneProgressStatus> EntityIUserSideStoryQuestSceneProgressStatus { get; set; } = [];
public List<EntityIUserStatus> EntityIUserStatus { get; set; } = [];
public List<EntityIUserThought> EntityIUserThought { get; set; } = [];
public List<EntityIUserTripleDeck> EntityIUserTripleDeck { get; set; } = [];
public List<EntityIUserTutorialProgress> EntityIUserTutorialProgress { get; set; } = [];
public List<EntityIUserWeapon> EntityIUserWeapon { get; set; } = [];
public List<EntityIUserWeaponAbility> EntityIUserWeaponAbility { get; set; } = [];
public List<EntityIUserWeaponAwaken> EntityIUserWeaponAwaken { get; set; } = [];
public List<EntityIUserWeaponNote> EntityIUserWeaponNote { get; set; } = [];
public List<EntityIUserWeaponSkill> EntityIUserWeaponSkill { get; set; } = [];
public List<EntityIUserWeaponStory> EntityIUserWeaponStory { get; set; } = [];
public List<EntityIUserWebviewPanelMission> EntityIUserWebviewPanelMission { get; set; } = [];
// Server-exclusive data (EntityS* prefix): never sent to client
public List<EntitySUser> EntitySUser { get; set; } = [];
public List<EntitySUserDevice> EntitySUserDevice { get; set; } = [];
public List<EntitySBattleDetail> BattleDetails { get; set; } = [];
public List<EntitySQuestSession> EntitySQuestSession { get; set; } = [];
public List<EntitySBigHuntSession> EntitySBigHuntSession { get; set; } = [];
public List<EntitySGachaBannerState> EntitySGachaBannerState { get; set; } = [];
public List<EntitySGachaRewardState> EntitySGachaRewardState { get; set; } = [];
}

67
src/Data/GameConfig.cs Normal file
View File

@@ -0,0 +1,67 @@
using MariesWonderland.Models.Entities;
namespace MariesWonderland.Data;
/// <summary>
/// Game-wide constants loaded from <see cref="EntityMConfig"/> master data at startup.
/// </summary>
public class GameConfig
{
public int ConsumableItemIdForGold { get; init; }
public int ConsumableItemIdForMedal { get; init; }
public int ConsumableItemIdForRareMedal { get; init; }
public int ConsumableItemIdForArenaCoin { get; init; }
public int ConsumableItemIdForExploreTicket { get; init; }
public int ConsumableItemIdForMomPoint { get; init; }
public int ConsumableItemIdForPremiumGachaTicket { get; init; }
public int ConsumableItemIdForQuestSkipTicket { get; init; }
public int CharacterRebirthAvailableCount { get; init; }
public int CharacterRebirthConsumeGold { get; init; }
public int CostumeAwakenAvailableCount { get; init; }
public int CostumeLimitBreakAvailableCount { get; init; }
public int MaterialSameWeaponExpCoefficientPermil { get; init; }
public int UserStaminaRecoverySecond { get; init; }
public int RewardGachaDailyMaxCount { get; init; }
public int QuestSkipMaxCountAtOnce { get; init; }
public int WeaponLimitBreakAvailableCount { get; init; }
/// <summary>Builds a <see cref="GameConfig"/> from the loaded master config rows.</summary>
public static GameConfig From(IEnumerable<EntityMConfig> configs)
{
Dictionary<string, string> kv = configs.ToDictionary(c => c.ConfigKey, c => c.Value);
return new GameConfig
{
ConsumableItemIdForGold = ParseInt(kv, "CONSUMABLE_ITEM_ID_FOR_GOLD"),
ConsumableItemIdForMedal = ParseInt(kv, "CONSUMABLE_ITEM_ID_FOR_MEDAL"),
ConsumableItemIdForRareMedal = ParseInt(kv, "CONSUMABLE_ITEM_ID_FOR_RARE_MEDAL"),
ConsumableItemIdForArenaCoin = ParseInt(kv, "CONSUMABLE_ITEM_ID_FOR_ARENA_COIN"),
ConsumableItemIdForExploreTicket = ParseInt(kv, "CONSUMABLE_ITEM_ID_FOR_EXPLORE_TICKET"),
ConsumableItemIdForMomPoint = ParseInt(kv, "CONSUMABLE_ITEM_ID_FOR_MOM_POINT"),
ConsumableItemIdForPremiumGachaTicket = ParseInt(kv, "CONSUMABLE_ITEM_ID_FOR_PREMIUM_GACHA_TICKET"),
ConsumableItemIdForQuestSkipTicket = ParseInt(kv, "CONSUMABLE_ITEM_ID_FOR_QUEST_SKIP_TICKET"),
CharacterRebirthAvailableCount = ParseInt(kv, "CHARACTER_REBIRTH_AVAILABLE_COUNT"),
CharacterRebirthConsumeGold = ParseInt(kv, "CHARACTER_REBIRTH_CONSUME_GOLD"),
CostumeAwakenAvailableCount = ParseInt(kv, "COSTUME_AWAKEN_AVAILABLE_COUNT"),
CostumeLimitBreakAvailableCount = ParseInt(kv, "COSTUME_LIMIT_BREAK_AVAILABLE_COUNT"),
MaterialSameWeaponExpCoefficientPermil = ParseInt(kv, "MATERIAL_SAME_WEAPON_EXP_COEFFICIENT_PERMIL"),
UserStaminaRecoverySecond = ParseInt(kv, "USER_STAMINA_RECOVERY_SECOND"),
RewardGachaDailyMaxCount = ParseInt(kv, "REWARD_GACHA_DAILY_MAX_COUNT"),
QuestSkipMaxCountAtOnce = ParseInt(kv, "QUEST_SKIP_MAX_COUNT_AT_ONCE"),
WeaponLimitBreakAvailableCount = ParseInt(kv, "WEAPON_LIMIT_BREAK_AVAILABLE_COUNT"),
};
}
private static int ParseInt(Dictionary<string, string> kv, string key)
=> kv.TryGetValue(key, out string? s) && int.TryParse(s, out int v) ? v : 0;
}

View File

@@ -0,0 +1,100 @@
using MariesWonderland.Proto.Data;
using System.Text.Json;
namespace MariesWonderland.Data;
/// <summary>
/// Computes the delta between before/after snapshots of a <see cref="DarkUserMemoryDatabase"/>
/// to produce incremental <see cref="DiffData"/> maps for gRPC responses. Uses reflection at
/// startup to discover all EntityI* (client-visible) list properties and builds per-table serializers.
/// </summary>
public static class UserDataDiffBuilder
{
private static readonly JsonSerializerOptions CamelCaseOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// Maps "IUser" -> func that serializes db.EntityIUser to JSON
private static readonly IReadOnlyDictionary<string, Func<DarkUserMemoryDatabase, string>> Serializers;
/// <summary>
/// Builds the table-name → serializer map once via reflection. Only EntityI* list properties
/// are included — EntityS* (server-only) and non-list properties are excluded.
/// </summary>
static UserDataDiffBuilder()
{
var serializers = new Dictionary<string, Func<DarkUserMemoryDatabase, string>>();
foreach (var prop in typeof(DarkUserMemoryDatabase).GetProperties())
{
// Only include client-visible user data tables (EntityI* prefix, List<> type)
if (!prop.Name.StartsWith("EntityI")) continue;
if (!prop.PropertyType.IsGenericType) continue;
if (prop.PropertyType.GetGenericTypeDefinition() != typeof(List<>)) continue;
// Strip the "Entity" prefix to get the table key the client expects (e.g. "IUserWeapon")
var tableName = prop.Name["Entity".Length..]; // "EntityIUser" -> "IUser"
var capturedProp = prop; // capture for lambda
serializers[tableName] = db =>
{
var list = capturedProp.GetValue(db);
return JsonSerializer.Serialize(list, CamelCaseOptions);
};
}
Serializers = serializers;
}
/// <summary>All client-visible user table names (IUser* series, excludes EntityS* and EntityM*).</summary>
public static IEnumerable<string> TableNames => Serializers.Keys;
/// <summary>
/// Captures the current state of all non-empty client-visible user tables as serialized JSON.
/// Use this before making changes; pass the result to Delta() after changes.
/// </summary>
public static Dictionary<string, string> Snapshot(DarkUserMemoryDatabase db)
{
var snapshot = new Dictionary<string, string>();
foreach (var (table, serialize) in Serializers)
{
var json = serialize(db);
if (json != "[]")
snapshot[table] = json;
}
return snapshot;
}
/// <summary>
/// Serializes a single named table to a JSON array string.
/// Returns "[]" if the table name is not recognised.
/// </summary>
public static string SerializeTable(DarkUserMemoryDatabase db, string tableName)
=> Serializers.TryGetValue(tableName, out var serialize) ? serialize(db) : "[]";
/// <summary>
/// Computes only the tables that changed since the snapshot.
/// Use this for incremental API responses (e.g. SetUserName).
/// </summary>
public static Dictionary<string, DiffData> Delta(Dictionary<string, string> before, DarkUserMemoryDatabase db)
{
Dictionary<string, DiffData> diff = [];
foreach (var (table, serialize) in Serializers)
{
// Serialize the current (post-mutation) state of this table
var afterJson = serialize(db);
before.TryGetValue(table, out var beforeJson);
beforeJson ??= "[]";
// Skip unchanged tables — only emit tables with actual modifications
if (afterJson == beforeJson) continue;
diff[table] = new DiffData
{
UpdateRecordsJson = afterJson,
DeleteKeysJson = "[]"
};
}
return diff;
}
}

View File

@@ -0,0 +1,57 @@
using MariesWonderland.Configuration;
using Microsoft.Extensions.Options;
using System.Text.Json;
namespace MariesWonderland.Data;
/// <summary>
/// Loads pre-seeded user data from Entity*Table.json files on disk into a <see cref="DarkUserMemoryDatabase"/>.
/// Uses reflection to match JSON file names to database properties, allowing new entity types to be
/// loaded without code changes.
/// </summary>
public class UserDataSeeder(IOptions<ServerOptions> options)
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private static readonly Type DbType = typeof(DarkUserMemoryDatabase);
/// <summary>
/// Reads all Entity*Table.json files from the configured UserDataPath and populates
/// a new DarkUserMemoryDatabase. Returns an empty database if no files are found.
/// </summary>
public DarkUserMemoryDatabase LoadFromFiles()
{
DarkUserMemoryDatabase db = new();
string rawPath = options.Value.Data.UserDataPath;
if (string.IsNullOrEmpty(rawPath))
return db;
string dataPath = Path.IsPathRooted(rawPath)
? rawPath
: Path.Combine(AppContext.BaseDirectory, rawPath);
if (!Directory.Exists(dataPath))
return db;
foreach (string file in Directory.EnumerateFiles(dataPath, "Entity*Table.json"))
{
// "EntityIUserCostumeTable.json" → "EntityIUserCostume"
string fileName = Path.GetFileName(file);
string propName = fileName[..^"Table.json".Length];
var prop = DbType.GetProperty(propName);
if (prop == null) continue;
string json = File.ReadAllText(file);
var list = JsonSerializer.Deserialize(json, prop.PropertyType, JsonOptions);
if (list != null)
prop.SetValue(db, list);
}
return db;
}
}

298
src/Data/UserDataStore.cs Normal file
View File

@@ -0,0 +1,298 @@
using MariesWonderland.Models.Entities;
using MariesWonderland.Models.Type;
using System.Text.Json;
namespace MariesWonderland.Data;
/// <summary>
/// Manages the map of userId → <see cref="DarkUserMemoryDatabase"/>. Handles user registration,
/// session management, and persistence to/from JSON files on disk.
/// </summary>
public class UserDataStore(DarkMasterMemoryDatabase masterDb)
{
private readonly DarkMasterMemoryDatabase _masterDb = masterDb;
private readonly Dictionary<long, DarkUserMemoryDatabase> _users = [];
private readonly Dictionary<string, long> _uuidToUserId = [];
private readonly Dictionary<string, UserSession> _sessions = [];
/// <summary>
/// Look up or create a user by UUID. Returns the userId and whether the user is new.
/// For new users, seeds the initial data into their database.
/// </summary>
public (long UserId, bool IsNew) RegisterOrGetUser(string uuid)
{
if (_uuidToUserId.TryGetValue(uuid, out var existingId))
return (existingId, false);
var userId = GenerateUserId();
_uuidToUserId[uuid] = userId;
var db = new DarkUserMemoryDatabase();
SeedInitialUserData(db, userId);
_users[userId] = db;
return (userId, true);
}
/// <summary>
/// Creates a new authenticated session for the user with the given TTL.
/// </summary>
public UserSession CreateSession(long userId, TimeSpan ttl)
{
var key = $"session_{userId}_{Guid.NewGuid():N}";
var session = new UserSession(key, userId, DateTime.UtcNow.Add(ttl));
_sessions[key] = session;
return session;
}
/// <summary>
/// Resolves a session key to a userId if the session exists and has not expired.
/// </summary>
public bool TryResolveSession(string sessionKey, out long userId)
{
if (_sessions.TryGetValue(sessionKey, out var session) && session.ExpiresAt > DateTime.UtcNow)
{
userId = session.UserId;
return true;
}
userId = 0;
return false;
}
/// <summary>
/// Returns the user's in-memory database, creating an empty one if not found.
/// </summary>
public DarkUserMemoryDatabase GetOrCreate(long userId)
{
if (!_users.TryGetValue(userId, out var db))
{
db = new DarkUserMemoryDatabase();
_users[userId] = db;
}
return db;
}
/// <summary>
/// Registers a pre-seeded database under a UUID. If the UUID is already mapped,
/// returns the existing userId without overwriting. Otherwise stores the seeded
/// database and maps the UUID to the userId found inside it.
/// </summary>
public long SeedUserFromDatabase(string uuid, DarkUserMemoryDatabase seededDb)
{
if (_uuidToUserId.TryGetValue(uuid, out var existingId))
return existingId;
long userId = seededDb.EntityIUser.FirstOrDefault()?.UserId ?? GenerateUserId();
_uuidToUserId[uuid] = userId;
_users[userId] = seededDb;
return userId;
}
/// <summary>
/// Attempts to retrieve a user's database without creating one.
/// </summary>
public bool TryGet(long userId, out DarkUserMemoryDatabase db)
=> _users.TryGetValue(userId, out db!);
/// <summary>
/// Stores a user database, replacing any existing one for that userId.
/// </summary>
public void Set(long userId, DarkUserMemoryDatabase db)
=> _users[userId] = db;
public IReadOnlyDictionary<long, DarkUserMemoryDatabase> All => _users;
/// <summary>
/// Serialize all user data to a JSON file on disk.
/// </summary>
public void Save(string filePath)
{
UserDataSnapshot snapshot = new()
{
Users = _users,
UuidToUserId = _uuidToUserId,
Sessions = _sessions.Values.ToList()
};
JsonSerializerOptions options = new()
{
WriteIndented = true
};
string json = JsonSerializer.Serialize(snapshot, options);
File.WriteAllText(filePath, json);
}
/// <summary>
/// Deserialize user data from a JSON file on disk, replacing the current in-memory state.
/// Returns the number of users loaded.
/// </summary>
public int Load(string filePath)
{
if (!File.Exists(filePath))
return 0;
string json = File.ReadAllText(filePath);
UserDataSnapshot? snapshot = JsonSerializer.Deserialize<UserDataSnapshot>(json);
if (snapshot is null)
return 0;
_users.Clear();
_uuidToUserId.Clear();
_sessions.Clear();
foreach (var (userId, db) in snapshot.Users)
_users[userId] = db;
foreach (var (uuid, userId) in snapshot.UuidToUserId)
_uuidToUserId[uuid] = userId;
foreach (UserSession session in snapshot.Sessions)
_sessions[session.SessionKey] = session;
return _users.Count;
}
/// <summary>
/// Generates a random 19-digit user ID.
/// </summary>
private static long GenerateUserId()
{
// Random 19-digit positive long (range: 1e18 to 2e18)
return Random.Shared.NextInt64(Constants.MinUserId, Constants.MaxUserId);
}
/// <summary>
/// Generates a random 12-digit player ID.
/// </summary>
private static long GeneratePlayerId()
{
// Random 12-digit positive long (range: 1e12 to 2e12)
return Random.Shared.NextInt64(Constants.MinPlayerId, Constants.MaxPlayerId);
}
/// <summary>
/// Populates a new user database with default records (profile, status, starting weapons, etc.).
/// </summary>
private void SeedInitialUserData(DarkUserMemoryDatabase db, long userId)
{
var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
db.EntityIUser.Add(new EntityIUser
{
UserId = userId,
PlayerId = GeneratePlayerId(),
OsType = 2,
PlatformType = PlatformType.GOOGLE_PLAY_STORE,
UserRestrictionType = 0,
RegisterDatetime = nowMs,
GameStartDatetime = nowMs
});
db.EntityIUserSetting.Add(new EntityIUserSetting
{
UserId = userId,
IsNotifyPurchaseAlert = false
});
db.EntityIUserStatus.Add(new EntityIUserStatus
{
UserId = userId,
Level = 1,
Exp = 0,
StaminaMilliValue = 60000,
StaminaUpdateDatetime = nowMs
});
db.EntityIUserProfile.Add(new EntityIUserProfile
{
UserId = userId,
Name = string.Empty,
NameUpdateDatetime = nowMs,
Message = string.Empty,
MessageUpdateDatetime = nowMs,
FavoriteCostumeId = 0,
FavoriteCostumeIdUpdateDatetime = nowMs
});
db.EntityIUserLogin.Add(new EntityIUserLogin
{
UserId = userId,
TotalLoginCount = 1,
ContinualLoginCount = 1,
MaxContinualLoginCount = 1,
LastLoginDatetime = nowMs,
LastComebackLoginDatetime = 0
});
db.EntityIUserLoginBonus.Add(new EntityIUserLoginBonus
{
UserId = userId,
LoginBonusId = 1,
CurrentPageNumber = 1,
CurrentStampNumber = 0,
LatestRewardReceiveDatetime = 0
});
db.EntityIUserTutorialProgress.Add(new EntityIUserTutorialProgress
{
UserId = userId,
TutorialType = TutorialType.GAME_START,
ProgressPhase = 0,
ChoiceId = 0
});
foreach (int weaponId in Constants.StartingWeaponIds)
{
string uuid = Guid.NewGuid().ToString();
db.EntityIUserWeapon.Add(new EntityIUserWeapon
{
UserId = userId,
UserWeaponUuid = uuid,
WeaponId = weaponId,
Level = 1,
Exp = 0,
LimitBreakCount = 0,
IsProtected = true,
AcquisitionDatetime = nowMs
});
db.EntityIUserWeaponNote.Add(new EntityIUserWeaponNote
{
UserId = userId,
WeaponId = weaponId,
MaxLevel = 1,
MaxLimitBreakCount = 0,
FirstAcquisitionDatetime = nowMs
});
db.EntityIUserWeaponStory.Add(new EntityIUserWeaponStory
{
UserId = userId,
WeaponId = weaponId,
ReleasedMaxStoryIndex = 1
});
EntityMWeapon? masterWeapon = _masterDb.EntityMWeapon.FirstOrDefault(w => w.WeaponId == weaponId);
if (masterWeapon != null)
{
foreach (EntityMWeaponAbilityGroup ag in _masterDb.EntityMWeaponAbilityGroup.Where(g => g.WeaponAbilityGroupId == masterWeapon.WeaponAbilityGroupId))
db.EntityIUserWeaponAbility.Add(new EntityIUserWeaponAbility { UserId = userId, UserWeaponUuid = uuid, SlotNumber = ag.SlotNumber, Level = 1 });
foreach (EntityMWeaponSkillGroup sg in _masterDb.EntityMWeaponSkillGroup.Where(g => g.WeaponSkillGroupId == masterWeapon.WeaponSkillGroupId))
db.EntityIUserWeaponSkill.Add(new EntityIUserWeaponSkill { UserId = userId, UserWeaponUuid = uuid, SlotNumber = sg.SlotNumber, Level = 1 });
}
}
}
}
/// <summary>
/// Serializable snapshot of all user data for persistence.
/// </summary>
file record UserDataSnapshot
{
public Dictionary<long, DarkUserMemoryDatabase> Users { get; init; } = [];
public Dictionary<string, long> UuidToUserId { get; init; } = [];
public List<UserSession> Sessions { get; init; } = [];
}

3
src/Data/UserSession.cs Normal file
View File

@@ -0,0 +1,3 @@
namespace MariesWonderland.Data;
public record UserSession(string SessionKey, long UserId, DateTime ExpiresAt);

View File

@@ -0,0 +1,25 @@
using Grpc.Core;
namespace MariesWonderland.Extensions;
/// <summary>
/// Extension methods for extracting client-provided headers from gRPC <see cref="ServerCallContext"/>.
/// The client sends userId and session key as custom metadata entries.
/// </summary>
public static class GrpcContextExtensions
{
/// <summary>
/// Reads the caller's user ID from the <c>x-apb-user-id</c> request header.
/// </summary>
public static long GetUserId(this ServerCallContext context)
{
string? value = context.RequestHeaders.GetValue("x-apb-user-id");
return value != null && long.TryParse(value, out long id) ? id : 0;
}
/// <summary>
/// Reads the caller's session key from the <c>x-apb-session-key</c> request header.
/// </summary>
public static string GetSessionKey(this ServerCallContext context)
=> context.RequestHeaders.GetValue("x-apb-session-key") ?? "";
}

View File

@@ -0,0 +1,58 @@
using MariesWonderland.Services;
namespace MariesWonderland.Extensions;
/// <summary>
/// Registers all gRPC service implementations with the ASP.NET Core endpoint routing pipeline.
/// Each <c>MapGrpcService</c> call wires a service class to its protobuf-defined RPC methods.
/// </summary>
public static class GrpcExtensions
{
/// <summary>
/// Registers all gRPC service endpoints on the application.
/// </summary>
public static WebApplication MapGrpcServices(this WebApplication app)
{
app.MapGrpcService<BannerService>();
app.MapGrpcService<BattleService>();
app.MapGrpcService<BigHuntService>();
app.MapGrpcService<CageOrnamentService>();
app.MapGrpcService<CharacterBoardService>();
app.MapGrpcService<CharacterService>();
app.MapGrpcService<CharacterViewerService>();
app.MapGrpcService<CompanionService>();
app.MapGrpcService<ConfigService>();
app.MapGrpcService<ConsumableItemService>();
app.MapGrpcService<ContentsStoryService>();
app.MapGrpcService<CostumeService>();
app.MapGrpcService<DataService>();
app.MapGrpcService<DeckService>();
app.MapGrpcService<DokanService>();
app.MapGrpcService<ExploreService>();
app.MapGrpcService<FriendService>();
app.MapGrpcService<GachaService>();
app.MapGrpcService<GameplayService>();
app.MapGrpcService<GiftService>();
app.MapGrpcService<GimmickService>();
app.MapGrpcService<IndividualPopService>();
app.MapGrpcService<LabyrinthService>();
app.MapGrpcService<LoginBonusService>();
app.MapGrpcService<MaterialService>();
app.MapGrpcService<MissionService>();
app.MapGrpcService<MovieService>();
app.MapGrpcService<NaviCutInService>();
app.MapGrpcService<NotificationService>();
app.MapGrpcService<OmikujiService>();
app.MapGrpcService<PartsService>();
app.MapGrpcService<PortalCageService>();
app.MapGrpcService<PvpService>();
app.MapGrpcService<QuestService>();
app.MapGrpcService<RewardService>();
app.MapGrpcService<ShopService>();
app.MapGrpcService<SideStoryQuestService>();
app.MapGrpcService<TutorialService>();
app.MapGrpcService<UserService>();
app.MapGrpcService<WeaponService>();
return app;
}
}

View File

@@ -0,0 +1,283 @@
using MariesWonderland.Configuration;
using MariesWonderland.Data;
using MariesWonderland.Http;
using Microsoft.Extensions.Options;
using System.Text;
namespace MariesWonderland.Extensions;
/// <summary>
/// Registers all non-gRPC HTTP endpoints: asset serving (list.bin, asset bundles, master database),
/// debug save/load utilities, terms of service, and the catch-all fallback route.
/// </summary>
public static class HttpApiExtensions
{
// The URL embedded in list.bin pointing to the original CDN (must be exactly 43 ASCII bytes).
private static readonly byte[] ResourcesUrlOriginal =
Encoding.ASCII.GetBytes("https://resources.app.nierreincarnation.com");
/// <summary>
/// Registers all HTTP endpoints, including asset serving, master database, debug utilities, and the catch-all route.
/// </summary>
public static WebApplication MapHttpApis(this WebApplication app)
{
ServerOptions options = app.Services.GetRequiredService<IOptions<ServerOptions>>().Value;
string assetDatabaseBasePath = options.Paths.AssetDatabase;
string masterDatabaseBasePath = options.Paths.MasterDatabase;
string resourcesBaseUrl = options.Paths.ResourcesBaseUrl;
string userDatabasePath = Path.IsPathRooted(options.Data.UserDatabase)
? options.Data.UserDatabase
: Path.Combine(AppContext.BaseDirectory, options.Data.UserDatabase);
ILogger<AssetDatabase> assetLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger<AssetDatabase>();
AssetDatabase assetDb = new(assetDatabaseBasePath, assetLogger);
app.MapGet("/", () => "Marie's Wonderland is open for business :marie:");
// Debug endpoints for manual save/load
app.MapGet("/debug/save", () =>
{
try
{
UserDataStore store = app.Services.GetRequiredService<UserDataStore>();
store.Save(userDatabasePath);
return Results.Ok($"Saved {store.All.Count} users to {userDatabasePath}");
}
catch (Exception ex)
{
return Results.Problem($"Save failed: {ex.Message}");
}
});
app.MapGet("/debug/load", () =>
{
try
{
UserDataStore store = app.Services.GetRequiredService<UserDataStore>();
int count = store.Load(userDatabasePath);
return count > 0
? Results.Ok($"Loaded {count} users from {userDatabasePath}")
: Results.Ok("No save file found.");
}
catch (Exception ex)
{
return Results.Problem($"Load failed: {ex.Message}");
}
});
// ToS. Expects the version wrapped in delimiters like "###123###".
app.MapGet("/web/static/{languagePath}/terms/termsofuse", (string languagePath) => $"<html><head><title>Terms of Service</title></head><body><h1>Terms of Service</h1><p>Language: {languagePath}</p><p>Version: ###123###</p></body></html>");
// Asset Database — serves list.bin, rewriting the embedded CDN base URL if configured.
// Records which revision the client is using so subsequent asset requests resolve correctly.
app.MapGet("/v1/list/300116832/{revision}", async (string revision, HttpContext ctx) =>
{
string clientIp = ctx.Connection.RemoteIpAddress?.ToString() ?? "";
assetDb.RememberRevision(clientIp, revision);
byte[] data = await File.ReadAllBytesAsync(Path.Combine(assetDatabaseBasePath, revision, "list.bin"));
RewriteResourcesUrl(data, resourcesBaseUrl, app.Logger);
return Results.Bytes(data, "application/x-protobuf");
});
// Asset Bundles / Resources — resolves objectId via the list.bin index for the client's active revision.
// Path: /aaaaaaaaaaaaaaaaaaaaaaaa/unso-{version}-{type}/{objectId}
// type = "assetbundle" or "resources" (last segment of "unso-…" after splitting on '-')
app.MapGet("/aaaaaaaaaaaaaaaaaaaaaaaa/unso-{version}-{type}/{objectId}", (string version, string type, string objectId, HttpContext ctx) =>
{
string clientIp = ctx.Connection.RemoteIpAddress?.ToString() ?? "";
foreach (AssetCandidate candidate in assetDb.Resolve(clientIp, type, objectId))
{
FileInfo info = new(candidate.Path);
if (!info.Exists) continue;
// Size validation: only enforce when list.bin provided a plausible size (≥ 256 bytes).
if (candidate.ExpectedSize >= 256 && info.Length != candidate.ExpectedSize)
{
app.Logger.LogDebug(
"Asset size mismatch: objectId={ObjectId} path={Path} expected={Expected} actual={Actual} — skipping",
objectId, candidate.Path, candidate.ExpectedSize, info.Length);
continue;
}
// MD5 validation when the index provided a checksum.
if (!string.IsNullOrEmpty(candidate.ExpectedMD5))
{
string? actualMd5 = assetDb.ComputeMd5(candidate.Path, info);
if (actualMd5 is not null && !string.Equals(actualMd5, candidate.ExpectedMD5, StringComparison.OrdinalIgnoreCase))
{
app.Logger.LogDebug(
"Asset MD5 mismatch: objectId={ObjectId} path={Path} expected={Expected} actual={Actual} source={Source} — skipping",
objectId, candidate.Path, candidate.ExpectedMD5, actualMd5, candidate.Source);
continue;
}
}
// Serve the file — Results.File handles Range requests, ETags, etc.
return Results.File(candidate.Path, "application/octet-stream");
}
app.Logger.LogWarning("Asset not found: objectId={ObjectId} type={Type} clientIp={Ip}", objectId, type, clientIp);
return Results.NotFound();
});
// Master Database
app.MapMethods("/assets/release/{masterVersion}/database.bin.e", ["GET", "HEAD"], async (HttpContext ctx, string masterVersion) =>
{
var filePath = Path.Combine(masterDatabaseBasePath, $"{masterVersion}.bin.e");
var fileInfo = new FileInfo(filePath);
long totalLength = fileInfo.Length;
// Advertise range support
ctx.Response.Headers.AcceptRanges = "bytes";
// Simple ETag using last write ticks & length (optional but useful for clients)
ctx.Response.Headers.ETag = $"\"{fileInfo.LastWriteTimeUtc.Ticks:x}-{totalLength:x}\"";
// Handle HEAD quickly (send headers only)
bool isHead = string.Equals(ctx.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase);
// Parse Range header if present
if (ctx.Request.Headers.TryGetValue("Range", out var rangeHeader))
{
// Expect single range of form: bytes=start-end
var raw = rangeHeader.ToString();
if (!raw.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase))
{
// Malformed range; respond with 416
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
var rangePart = raw["bytes=".Length..].Trim();
var parts = rangePart.Split('-', 2);
if (parts.Length != 2)
{
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
bool startParsed = long.TryParse(parts[0], out long start);
bool endParsed = long.TryParse(parts[1], out long end);
if (!startParsed && endParsed)
{
// suffix range: last 'end' bytes
long suffixLength = end;
if (suffixLength <= 0)
{
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
start = Math.Max(0, totalLength - suffixLength);
end = totalLength - 1;
}
else if (startParsed && !endParsed)
{
// range from start to end of file
end = totalLength - 1;
}
else if (!startParsed && !endParsed)
{
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
// Validate
if (start < 0 || end < start || start >= totalLength)
{
ctx.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable;
ctx.Response.Headers.ContentRange = $"bytes */{totalLength}";
return;
}
long length = end - start + 1;
ctx.Response.StatusCode = StatusCodes.Status206PartialContent;
ctx.Response.Headers.ContentRange = $"bytes {start}-{end}/{totalLength}";
ctx.Response.ContentType = "application/octet-stream";
ctx.Response.ContentLength = length;
if (isHead)
{
return;
}
// Stream the requested range
await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
fs.Seek(start, SeekOrigin.Begin);
var buffer = new byte[64 * 1024];
long remaining = length;
while (remaining > 0)
{
int toRead = (int)Math.Min(buffer.Length, remaining);
int read = await fs.ReadAsync(buffer.AsMemory(0, toRead));
if (read == 0) break;
await ctx.Response.Body.WriteAsync(buffer.AsMemory(0, read));
remaining -= read;
}
return;
}
// No Range header: return full file
ctx.Response.StatusCode = StatusCodes.Status200OK;
ctx.Response.ContentType = "application/octet-stream";
ctx.Response.ContentLength = totalLength;
if (isHead)
{
return;
}
// Stream the whole file
await using var fullFs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await fullFs.CopyToAsync(ctx.Response.Body);
});
// Catch all
app.MapMethods("/{**catchAll}", ["GET", "POST", "PUT", "DELETE", "PATCH",], (HttpContext ctx, string catchAll) =>
{
app.Logger.LogWarning("Catchall endpoint hit for {Method} {Path}", ctx.Request.Method, ctx.Request.Path);
return $"You requested: {catchAll}";
});
return app;
}
/// <summary>
/// Rewrites the CDN base URL embedded in list.bin bytes to the configured local URL.
/// The replacement must be exactly 43 ASCII bytes to match the original protobuf field length.
/// </summary>
private static void RewriteResourcesUrl(byte[] data, string replacementUrl, ILogger logger)
{
if (string.IsNullOrEmpty(replacementUrl))
return;
byte[] replacement = Encoding.ASCII.GetBytes(replacementUrl);
if (replacement.Length != ResourcesUrlOriginal.Length)
{
logger.LogWarning(
"ResourcesBaseUrl is {Length} bytes but must be exactly {Required} bytes — serving list.bin unmodified.",
replacement.Length, ResourcesUrlOriginal.Length);
return;
}
int idx = data.AsSpan().IndexOf(ResourcesUrlOriginal);
if (idx < 0)
{
logger.LogWarning("CDN URL not found in list.bin — serving unmodified.");
return;
}
replacement.CopyTo(data, idx);
logger.LogDebug("list.bin: rewrote resource base URL to {Url}", replacementUrl);
}
}

View File

@@ -0,0 +1,39 @@
using MariesWonderland.Configuration;
using MariesWonderland.Data;
namespace MariesWonderland.Extensions;
/// <summary>
/// Dependency injection setup for server configuration and data stores.
/// </summary>
public static class ServiceExtensions
{
/// <summary>
/// Binds the <see cref="ServerOptions"/> configuration section to DI.
/// </summary>
public static IServiceCollection AddServerOptions(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<ServerOptions>(configuration.GetSection(ServerOptions.SectionName));
return services;
}
/// <summary>
/// Loads the master database and registers <see cref="UserDataStore"/> and related singletons.
/// </summary>
public static IServiceCollection AddDataStores(this IServiceCollection services, IConfiguration configuration)
{
var options = configuration.GetSection(ServerOptions.SectionName).Get<ServerOptions>()!;
var binPath = Path.Combine(options.Paths.MasterDatabase, $"{options.Data.LatestMasterDataVersion}.bin.e");
var masterDb = BinaryMasterDataLoader.Load(binPath);
var gameConfig = GameConfig.From(masterDb.EntityMConfig);
services.AddSingleton(masterDb);
services.AddSingleton(gameConfig);
services.AddSingleton<UserDataStore>();
services.AddSingleton<UserDataSeeder>();
return services;
}
}

View File

@@ -0,0 +1,38 @@
using MariesWonderland.Models.Entities;
namespace MariesWonderland.Helpers;
/// <summary>
/// Extension methods on <see cref="List{T}"/> for adding and retrieving user entities,
/// supporting both predicate-based and <see cref="IUserEntity.UserId"/>-based lookups with lazy creation.
/// </summary>
public static class EntityHelper
{
/// <summary>Adds an entity to a list and returns it (convenience for inline new-entity seeding).</summary>
public static T AddNew<T>(this List<T> list, T entity)
{
list.Add(entity);
return entity;
}
/// <summary>Returns the first element matching <paramref name="predicate"/>, creating and adding one via <paramref name="factory"/> if none match.</summary>
public static T GetOrCreate<T>(this List<T> list, Func<T, bool> predicate, Func<T> factory)
{
T? existing = list.FirstOrDefault(predicate);
if (existing is not null) return existing;
T entity = factory();
list.Add(entity);
return entity;
}
/// <summary>Returns the first element matching by <see cref="IUserEntity.UserId"/>, creating and adding one via <paramref name="factory"/> if none match.</summary>
public static T GetOrCreate<T>(this List<T> list, long userId, Func<T>? factory = null)
where T : IUserEntity, new()
{
T? existing = list.FirstOrDefault(e => e.UserId == userId);
if (existing is not null) return existing;
T entity = factory is not null ? factory() : new T { UserId = userId };
list.Add(entity);
return entity;
}
}

View File

@@ -0,0 +1,168 @@
using MariesWonderland.Data;
using MariesWonderland.Models.Entities;
using MariesWonderland.Models.Type;
namespace MariesWonderland.Helpers;
/// <summary>
/// Central handler for granting possessions to users. Consolidates the PossessionType switch
/// logic that was duplicated across QuestService, GachaService, RewardService, and TutorialService.
/// </summary>
public static class PossessionHelper
{
/// <summary>
/// Grants a possession to the user by type, delegating to type-specific handlers.
/// </summary>
public static void Apply(DarkUserMemoryDatabase userDb, long userId, PossessionType type, int id, int count, DarkMasterMemoryDatabase masterDb)
{
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
switch (type)
{
case PossessionType.FREE_GEM:
userDb.EntityIUserGem[0].FreeGem += count;
break;
case PossessionType.PAID_GEM:
userDb.EntityIUserGem[0].PaidGem += count;
break;
case PossessionType.MATERIAL:
{
EntityIUserMaterial? mat = userDb.EntityIUserMaterial.FirstOrDefault(m => m.MaterialId == id);
if (mat == null)
userDb.EntityIUserMaterial.Add(new EntityIUserMaterial { UserId = userId, MaterialId = id, Count = count, FirstAcquisitionDatetime = nowMs });
else
mat.Count += count;
break;
}
case PossessionType.CONSUMABLE_ITEM:
{
EntityIUserConsumableItem? item = userDb.EntityIUserConsumableItem.FirstOrDefault(c => c.ConsumableItemId == id);
if (item == null)
userDb.EntityIUserConsumableItem.Add(new EntityIUserConsumableItem { UserId = userId, ConsumableItemId = id, Count = count, FirstAcquisitionDatetime = nowMs });
else
item.Count += count;
break;
}
case PossessionType.IMPORTANT_ITEM:
{
EntityIUserImportantItem? item = userDb.EntityIUserImportantItem.FirstOrDefault(c => c.ImportantItemId == id);
if (item == null)
userDb.EntityIUserImportantItem.Add(new EntityIUserImportantItem { UserId = userId, ImportantItemId = id, Count = count, FirstAcquisitionDatetime = nowMs });
else
item.Count += count;
break;
}
case PossessionType.PREMIUM_ITEM:
{
EntityIUserPremiumItem? item = userDb.EntityIUserPremiumItem.FirstOrDefault(p => p.PremiumItemId == id);
if (item == null)
userDb.EntityIUserPremiumItem.Add(new EntityIUserPremiumItem { UserId = userId, PremiumItemId = id, AcquisitionDatetime = nowMs });
else
item.AcquisitionDatetime = nowMs;
break;
}
case PossessionType.WEAPON:
case PossessionType.WEAPON_ENHANCED:
WeaponHelper.GrantWeapon(userDb, userId, id, masterDb);
break;
case PossessionType.COSTUME:
case PossessionType.COSTUME_ENHANCED:
GrantCostume(userDb, userId, id, masterDb);
break;
case PossessionType.COMPANION:
GrantCompanion(userDb, userId, id);
break;
case PossessionType.PARTS:
GrantParts(userDb, userId, id, masterDb);
break;
}
}
/// <summary>
/// Grants a costume to the user, unlocking the associated character if not already owned.
/// </summary>
public static void GrantCostume(DarkUserMemoryDatabase userDb, long userId, int costumeId, DarkMasterMemoryDatabase masterDb)
{
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string uuid = Guid.NewGuid().ToString();
userDb.EntityIUserCostume.Add(new EntityIUserCostume
{
UserId = userId,
UserCostumeUuid = uuid,
CostumeId = costumeId,
Level = 1,
HeadupDisplayViewId = 1,
AcquisitionDatetime = nowMs
});
// Auto-unlock the character tied to this costume if not already owned
EntityMCostume? masterCostume = masterDb.EntityMCostume.FirstOrDefault(c => c.CostumeId == costumeId);
if (masterCostume != null && !userDb.EntityIUserCharacter.Any(c => c.CharacterId == masterCostume.CharacterId))
userDb.EntityIUserCharacter.Add(new EntityIUserCharacter { UserId = userId, CharacterId = masterCostume.CharacterId, Level = 1 });
userDb.EntityIUserCostumeActiveSkill.Add(new EntityIUserCostumeActiveSkill { UserId = userId, UserCostumeUuid = uuid, Level = 1, AcquisitionDatetime = nowMs });
}
/// <summary>
/// Grants a companion to the user. Skips if the companion is already owned.
/// </summary>
public static void GrantCompanion(DarkUserMemoryDatabase userDb, long userId, int companionId)
{
if (userDb.EntityIUserCompanion.Any(c => c.CompanionId == companionId)) return;
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
userDb.EntityIUserCompanion.Add(new EntityIUserCompanion
{
UserId = userId,
UserCompanionUuid = Guid.NewGuid().ToString(),
CompanionId = companionId,
Level = 1,
HeadupDisplayViewId = 1,
AcquisitionDatetime = nowMs
});
}
/// <summary>
/// Grants a single Parts item to the user. Skips if the user already owns a part with the same PartsId.
/// </summary>
public static void GrantParts(DarkUserMemoryDatabase userDb, long userId, int partsId, DarkMasterMemoryDatabase masterDb)
{
if (userDb.EntityIUserParts.Any(p => p.PartsId == partsId)) return;
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
EntityMParts? partsDef = masterDb.EntityMParts.FirstOrDefault(p => p.PartsId == partsId);
// Derive main stat ID from the lottery group: tens digit = tier, ones digit = category
int mainStatId = 0;
if (partsDef != null)
{
int groupId = partsDef.PartsStatusMainLotteryGroupId;
if (groupId > 0)
{
int tier = groupId / 10;
int category = groupId % 10;
mainStatId = (category - 1) * 4 + tier;
}
if (!userDb.EntityIUserPartsGroupNote.Any(n => n.PartsGroupId == partsDef.PartsGroupId))
{
userDb.EntityIUserPartsGroupNote.Add(new EntityIUserPartsGroupNote
{
UserId = userId,
PartsGroupId = partsDef.PartsGroupId,
FirstAcquisitionDatetime = nowMs
});
}
}
userDb.EntityIUserParts.Add(new EntityIUserParts
{
UserId = userId,
UserPartsUuid = Guid.NewGuid().ToString(),
PartsId = partsId,
Level = 1,
PartsStatusMainId = mainStatId,
AcquisitionDatetime = nowMs
});
}
}

View File

@@ -0,0 +1,77 @@
using MariesWonderland.Data;
using MariesWonderland.Models.Entities;
using MariesWonderland.Models.Type;
namespace MariesWonderland.Helpers;
/// <summary>
/// Creates all entities associated with granting a weapon: the weapon instance, note record,
/// ability/skill slots from master data, and story unlocks triggered by the ACQUISITION condition.
/// </summary>
public static class WeaponHelper
{
/// <summary>
/// Grants a weapon to the user: creates EntityIUserWeapon, EntityIUserWeaponNote (if new),
/// ability/skill slots, and unlocks weapon stories for the ACQUISITION condition.
/// </summary>
public static void GrantWeapon(DarkUserMemoryDatabase userDb, long userId, int weaponId, DarkMasterMemoryDatabase masterDb)
{
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string uuid = Guid.NewGuid().ToString();
userDb.EntityIUserWeapon.Add(new EntityIUserWeapon
{
UserId = userId,
UserWeaponUuid = uuid,
WeaponId = weaponId,
Level = 1,
AcquisitionDatetime = nowMs
});
// WeaponNote tracks per-weaponId metadata (max level, first acquisition); create only for the first copy
if (!userDb.EntityIUserWeaponNote.Any(n => n.WeaponId == weaponId))
{
userDb.EntityIUserWeaponNote.Add(new EntityIUserWeaponNote
{
UserId = userId,
WeaponId = weaponId,
MaxLevel = 1,
FirstAcquisitionDatetime = nowMs
});
}
// Look up master definition to populate ability/skill slots
EntityMWeapon? masterWeapon = masterDb.EntityMWeapon.FirstOrDefault(w => w.WeaponId == weaponId);
if (masterWeapon == null) return;
// Create one ability slot per entry in the weapon's ability group
foreach (EntityMWeaponAbilityGroup ag in masterDb.EntityMWeaponAbilityGroup.Where(g => g.WeaponAbilityGroupId == masterWeapon.WeaponAbilityGroupId))
userDb.EntityIUserWeaponAbility.Add(new EntityIUserWeaponAbility { UserId = userId, UserWeaponUuid = uuid, SlotNumber = ag.SlotNumber, Level = 1 });
// Create one skill slot per entry in the weapon's skill group
foreach (EntityMWeaponSkillGroup sg in masterDb.EntityMWeaponSkillGroup.Where(g => g.WeaponSkillGroupId == masterWeapon.WeaponSkillGroupId))
userDb.EntityIUserWeaponSkill.Add(new EntityIUserWeaponSkill { UserId = userId, UserWeaponUuid = uuid, SlotNumber = sg.SlotNumber, Level = 1 });
// Unlock weapon stories for ACQUISITION condition
if (masterWeapon.WeaponStoryReleaseConditionGroupId != 0)
{
foreach (EntityMWeaponStoryReleaseConditionGroup condRow in masterDb.EntityMWeaponStoryReleaseConditionGroup
.Where(c => c.WeaponStoryReleaseConditionGroupId == masterWeapon.WeaponStoryReleaseConditionGroupId
&& c.WeaponStoryReleaseConditionType == WeaponStoryReleaseConditionType.ACQUISITION
&& c.ConditionValue == 0))
{
GrantWeaponStory(userDb, masterWeapon.WeaponId, condRow.StoryIndex, userId);
}
}
}
/// <summary>Creates or updates a weapon story unlock record.</summary>
public static void GrantWeaponStory(DarkUserMemoryDatabase userDb, int weaponId, int storyIndex, long userId)
{
EntityIUserWeaponStory? existing = userDb.EntityIUserWeaponStory.FirstOrDefault(s => s.WeaponId == weaponId);
if (existing == null)
userDb.EntityIUserWeaponStory.Add(new EntityIUserWeaponStory { UserId = userId, WeaponId = weaponId, ReleasedMaxStoryIndex = storyIndex });
else
existing.ReleasedMaxStoryIndex = Math.Max(existing.ReleasedMaxStoryIndex, storyIndex);
}
}

434
src/Http/AssetDatabase.cs Normal file
View File

@@ -0,0 +1,434 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MariesWonderland.Http;
/// <summary>
/// Resolves asset bundle/resource requests by parsing list.bin protobuf indexes and info.json alias maps.
///
/// <para>
/// Asset revisions are deltas, but revision 0 is a complete superset: it contains every objectId
/// that appears across all 818 revisions (confirmed by exhaustive analysis). Later revisions carry
/// updated versions of existing assets, not new ones. <see cref="Resolve"/> therefore checks the
/// client's current revision first, then falls back to revision 0 — a single 25 MB index that
/// covers the entire asset catalogue.
/// </para>
/// </summary>
public sealed class AssetDatabase(string basePath, ILogger<AssetDatabase> logger)
{
// Lazy-initialized per-revision list.bin indexes (objectId → entry).
private readonly ConcurrentDictionary<string, Lazy<Dictionary<string, ListBinEntry>>> _listBinCache = new();
// Lazy-initialized per-revision info.json alias maps (fromName → alias target).
private readonly ConcurrentDictionary<string, Lazy<Dictionary<string, InfoAlias>?>> _infoCache = new();
// Per-client-IP active revision (set when client fetches list.bin).
private readonly ConcurrentDictionary<string, string> _clientRevisions = new();
// Fallback when no per-client revision is known.
private volatile string _lastKnownRevision = "0";
/// <summary>Records that a client fetched list.bin for the given revision.</summary>
public void RememberRevision(string clientIp, string revision)
{
_clientRevisions[clientIp] = revision;
_lastKnownRevision = revision;
}
/// <summary>
/// Resolves an asset request to ordered file-path candidates.
/// Caller should try each candidate in order, validating size and MD5, and serve the first valid one.
/// </summary>
/// <remarks>
/// The client's current revision is checked first. If the objectId is not present (e.g. a
/// recent revision carries only updated entries, not the full catalogue), revision 0 is used
/// as the fallback. Revision 0 is a confirmed superset of all objectIds across every revision.
/// </remarks>
public IEnumerable<AssetCandidate> Resolve(string clientIp, string assetType, string objectId)
{
string revision = _clientRevisions.TryGetValue(clientIp, out string? rev) ? rev : _lastKnownRevision;
// If the objectId isn't in the client's current revision, fall back to revision 0.
// Revision 0 is a complete superset — every objectId in the game is present there.
Dictionary<string, ListBinEntry>? currentIndex = LoadListBinIndex(revision);
if ((currentIndex is null || !currentIndex.ContainsKey(objectId)) && revision != "0")
revision = "0";
return ResolveForRevision(revision, assetType, objectId);
}
private IEnumerable<AssetCandidate> ResolveForRevision(string revision, string assetType, string objectId)
{
Dictionary<string, ListBinEntry>? index = LoadListBinIndex(revision);
if (index is null || !index.TryGetValue(objectId, out ListBinEntry? entry))
yield break;
List<(string Path, bool IsLocaleFallback)> primaryPaths = BuildCandidatePaths(revision, assetType, entry.Path);
HashSet<string> seen = [];
foreach ((string path, bool isLocaleFallback) in primaryPaths)
{
if (!seen.Add(path))
{
continue;
}
yield return new AssetCandidate(path, revision, "list.bin", isLocaleFallback ? "" : entry.MD5, entry.Size, isLocaleFallback);
}
// info.json alias redirects: if the file name maps to a different target (possibly in another revision)
Dictionary<string, InfoAlias>? infoIndex = LoadInfoIndex(revision);
if (infoIndex is not null)
{
foreach ((string path, bool _) in primaryPaths)
{
string baseName = Path.GetFileName(path);
if (!infoIndex.TryGetValue(baseName, out InfoAlias? alias)) continue;
string targetRevision = alias.ToRevision ?? revision;
string? altPath = BuildAliasPath(path, revision, assetType, targetRevision, alias.ToName);
if (altPath is null) continue;
string cacheKey = $"{targetRevision}:{altPath}";
if (!seen.Add(cacheKey)) continue;
yield return new AssetCandidate(altPath, targetRevision, "info.json redirect", alias.MD5 ?? "", 0);
}
}
}
/// <summary>
/// Builds candidate filesystem paths for a list.bin path string.
/// Original path first, then locale fallbacks. Non-ASCII paths also get mojibake/fullwidth variants.
/// </summary>
private List<(string Path, bool IsLocaleFallback)> BuildCandidatePaths(string revision, string assetType, string pathStr)
{
// Safety check on raw path before any substitution
string rawFsPath = pathStr.Replace(')', '/');
if (rawFsPath.Contains("..") || Path.IsPathRooted(rawFsPath))
{
return [];
}
// Build tagged entries: original first, then mojibake/fullwidth variants, then locale fallbacks
List<(string PathStr, bool IsLocaleFallback)> entries = [(pathStr, false)];
if (HasNonAscii(pathStr))
{
entries.Add((Utf8ToMojibake(pathStr), false));
entries.Add((NormalizeFullwidth(pathStr), false));
}
if (pathStr.Contains(")ja)"))
{
entries.Add((pathStr.Replace(")ja)", ")en)"), true));
}
if (pathStr.Contains(")ko)"))
{
entries.Add((pathStr.Replace(")ko)", ")en)"), true));
}
List<(string Path, bool IsLocaleFallback)> result = [];
HashSet<string> seen = [];
foreach ((string variant, bool isLocaleFallback) in entries)
{
string cleaned = variant.Replace(')', '/');
if (cleaned.Contains("..") || Path.IsPathRooted(cleaned))
{
continue;
}
if (!seen.Add(cleaned))
{
continue;
}
string fullPath = assetType switch
{
"assetbundle" => Path.Combine(basePath, revision, "assetbundle", cleaned + ".assetbundle"),
"resources" => Path.Combine(basePath, revision, "resources", cleaned),
_ => null!
};
if (fullPath is not null)
{
result.Add((fullPath, isLocaleFallback));
}
}
return result;
}
private static bool HasNonAscii(string s) => s.Any(c => c >= '\x80');
// Re-encodes non-ASCII chars as if each UTF-8 byte were a Latin-1 codepoint (double-encoding).
// Matches filenames extracted by tools that misinterpret UTF-8 paths as Latin-1.
private static string Utf8ToMojibake(string s) =>
new string(Encoding.UTF8.GetBytes(s).Select(b => (char)b).ToArray());
// Replaces fullwidth Unicode chars (U+FF01U+FF5E) with their ASCII equivalents (U+0021U+007E).
private static string NormalizeFullwidth(string s) =>
new string(s.Select(c => c is >= '\uFF01' and <= '\uFF5E' ? (char)(c - 0xFF01 + 0x21) : c).ToArray());
/// <summary>
/// Builds the filesystem path for an info.json alias: same directory structure, different revision + filename.
/// </summary>
private string? BuildAliasPath(string originalPath, string originalRevision, string assetType,
string targetRevision, string targetName)
{
string typeRoot = Path.Combine(basePath, originalRevision, assetType);
string rel = Path.GetRelativePath(typeRoot, originalPath);
if (rel.StartsWith("..") || Path.IsPathRooted(rel)) return null;
string dir = Path.GetDirectoryName(rel) ?? "";
return Path.Combine(basePath, targetRevision, assetType, dir, targetName);
}
private Dictionary<string, ListBinEntry>? LoadListBinIndex(string revision)
{
Lazy<Dictionary<string, ListBinEntry>> lazy = _listBinCache.GetOrAdd(
revision, rev => new Lazy<Dictionary<string, ListBinEntry>>(() =>
{
string path = Path.Combine(basePath, rev, "list.bin");
if (!File.Exists(path)) return [];
byte[] data = File.ReadAllBytes(path);
Dictionary<string, ListBinEntry> index = ListBinParser.Parse(data.AsSpan());
logger.LogDebug("Loaded list.bin for revision {Revision}: {Count} entries", rev, index.Count);
return index;
}));
return lazy.Value;
}
private Dictionary<string, InfoAlias>? LoadInfoIndex(string revision)
{
Lazy<Dictionary<string, InfoAlias>?> lazy = _infoCache.GetOrAdd(
revision, rev => new Lazy<Dictionary<string, InfoAlias>?>(() =>
{
string path = Path.Combine(basePath, rev, "info.json");
if (!File.Exists(path)) return null;
try
{
InfoJsonEntry[]? entries = JsonSerializer.Deserialize<InfoJsonEntry[]>(File.ReadAllText(path));
if (entries is null) return null;
Dictionary<string, InfoAlias> result = [];
foreach (InfoJsonEntry e in entries)
{
if (!string.IsNullOrEmpty(e.FromName) && !string.IsNullOrEmpty(e.ToName))
result[e.FromName] = new InfoAlias(e.ToName, e.ToRevision?.ToString(), e.MD5);
}
return result;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to parse info.json for revision {Revision}", rev);
return null;
}
}));
return lazy.Value;
}
// MD5 cache: path → (size, modTimeUtcTicks, md5Hex)
private readonly ConcurrentDictionary<string, (long Size, long ModTimeTicks, string Md5)> _md5Cache = new();
/// <summary>
/// Computes and caches the MD5 hex digest for a file, using cached result when size and modification time are unchanged.
/// </summary>
public string? ComputeMd5(string filePath, FileInfo info)
{
long modTicks = info.LastWriteTimeUtc.Ticks;
if (_md5Cache.TryGetValue(filePath, out (long Size, long ModTimeTicks, string Md5) cached)
&& cached.Size == info.Length && cached.ModTimeTicks == modTicks)
{
return cached.Md5;
}
try
{
byte[] hash = MD5.HashData(File.ReadAllBytes(filePath));
string hex = Convert.ToHexStringLower(hash);
_md5Cache[filePath] = (info.Length, modTicks, hex);
return hex;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to compute MD5 for {FilePath}", filePath);
return null;
}
}
}
public record ListBinEntry(string Path, long Size, string MD5);
public record AssetCandidate(string Path, string Revision, string Source, string ExpectedMD5, long ExpectedSize, bool IsLocaleFallback = false);
public record InfoAlias(string ToName, string? ToRevision, string? MD5);
public sealed class InfoJsonEntry
{
[JsonPropertyName("from-name")] public string FromName { get; init; } = "";
[JsonPropertyName("to-name")] public string ToName { get; init; } = "";
[JsonPropertyName("to-revision")] public int? ToRevision { get; init; }
[JsonPropertyName("md5")] public string? MD5 { get; init; }
}
/// <summary>
/// Parses the Octo asset management list.bin binary into a dictionary of objectId → asset entry.
///
/// <para>
/// <b>list.bin outer format</b> — a protobuf message with mixed fields:
/// <list type="bullet">
/// <item>Field 1 (varint): revision number (header metadata, skipped)</item>
/// <item>Field 2 (repeated, length-delimited): one entry per asset</item>
/// </list>
/// The outer loop treats every length-delimited field as a potential entry regardless of field
/// number, skipping non-length-delimited fields.
/// </para>
///
/// <para>
/// <b>Entry inner format</b>:
/// <list type="bullet">
/// <item>Field 1 (varint): category index (ignored)</item>
/// <item>Field 3 (string): path, using <c>)</c> as directory separator</item>
/// <item>Field 4 (varint): file size in bytes</item>
/// <item>Field 5 (varint): CRC / hash (ignored)</item>
/// <item>Field 6 (varint): unknown (ignored)</item>
/// <item>Field 9 (varint): asset type index (ignored)</item>
/// <item>Field 10 (string): MD5 hex digest</item>
/// <item>Field 11 (string): objectId — 6-byte ASCII key used in asset request URLs</item>
/// <item>Field 12 (varint): timestamp (8-byte varint — <b>requires reading up to 10 varint bytes</b>)</item>
/// <item>Field 13 (varint): revision number (ignored)</item>
/// </list>
/// </para>
///
/// <para>
/// <b>Important</b>: varints in list.bin can be up to 8 bytes (field 12 carries a Unix timestamp
/// in milliseconds). The reader must not bail out after 5 bytes (35-bit limit) like a naive
/// int32 varint reader would — it must continue reading up to 10 bytes, discarding high bits
/// that do not fit in int32, as the source format uses 64-bit integers.
/// </para>
/// </summary>
internal static class ListBinParser
{
/// <summary>
/// Parses the list.bin binary data into a dictionary mapping objectId to asset entry.
/// </summary>
public static Dictionary<string, ListBinEntry> Parse(ReadOnlySpan<byte> data)
{
Dictionary<string, ListBinEntry> idx = [];
int pos = 0;
while (pos < data.Length)
{
if (!TryReadVarint(data, ref pos, out int tag)) break;
int wireType = tag & 0x7;
if (wireType == 2)
{
if (!TryReadVarint(data, ref pos, out int length) || length < 0 || pos + length > data.Length)
break;
// Always advance past this field, whether or not the entry parses successfully.
int entryStart = pos;
if (TryParseEntry(data.Slice(pos, length), out string? objectId, out ListBinEntry entry) && objectId != null)
idx[objectId] = entry;
pos = entryStart + length;
}
else
{
// Skip varint / fixed-width outer fields (e.g. field 1 = revision header).
if (!TrySkipField(wireType, data, ref pos)) break;
}
}
return idx;
}
private static bool TryParseEntry(ReadOnlySpan<byte> data, out string? objectId, out ListBinEntry entry)
{
objectId = null;
string path = "";
long size = 0;
string md5 = "";
int pos = 0;
while (pos < data.Length)
{
if (!TryReadVarint(data, ref pos, out int tag)) { entry = default!; return false; }
int fieldNum = tag >> 3;
int wireType = tag & 0x7;
switch (fieldNum)
{
case 3: // path
if (wireType != 2 || !TryReadString(data, ref pos, out path)) { entry = default!; return false; }
break;
case 4: // size (varint)
if (wireType != 0 || !TryReadVarint(data, ref pos, out int sz)) { entry = default!; return false; }
if (sz >= 256) size = sz;
break;
case 10: // md5
if (wireType != 2 || !TryReadString(data, ref pos, out md5)) { entry = default!; return false; }
break;
case 11: // objectId
if (wireType != 2 || !TryReadString(data, ref pos, out string oid)) { entry = default!; return false; }
objectId = oid;
break;
default:
// Unknown field — skip and continue
if (!TrySkipField(wireType, data, ref pos)) { entry = default!; return false; }
break;
}
}
if (objectId is null || string.IsNullOrEmpty(path)) { entry = default!; return false; }
entry = new ListBinEntry(path, size, md5);
return true;
}
private static bool TryReadString(ReadOnlySpan<byte> data, ref int pos, out string value)
{
value = "";
if (!TryReadVarint(data, ref pos, out int length) || length < 0 || pos + length > data.Length)
return false;
value = Encoding.UTF8.GetString(data.Slice(pos, length));
pos += length;
return true;
}
/// <summary>Reads a protobuf varint from <paramref name="data"/> at <paramref name="pos"/>, advancing pos.</summary>
private static bool TryReadVarint(ReadOnlySpan<byte> data, ref int pos, out int value)
{
value = 0;
int shift = 0;
while (pos < data.Length)
{
byte b = data[pos++];
// Accumulate into value only while bits fit in int32; still read (and discard) higher bytes.
if (shift < 32)
value |= (b & 0x7F) << shift;
if ((b & 0x80) == 0) return true;
shift += 7;
if (shift >= 70) return false; // max 10 bytes
}
return false;
}
private static bool TrySkipField(int wireType, ReadOnlySpan<byte> data, ref int pos)
{
switch (wireType)
{
case 0: return TryReadVarint(data, ref pos, out _);
case 1: if (pos + 8 > data.Length) return false; pos += 8; return true;
case 2:
if (!TryReadVarint(data, ref pos, out int len) || len < 0 || pos + len > data.Length) return false;
pos += len; return true;
case 5: if (pos + 4 > data.Length) return false; pos += 4; return true;
default: return false;
}
}
}

View File

@@ -0,0 +1,59 @@
using Grpc.Core;
using Grpc.Core.Interceptors;
using MariesWonderland.Data;
using MariesWonderland.Extensions;
using System.Text.Json;
namespace MariesWonderland.Interceptors;
/// <summary>
/// gRPC interceptor that persists the user's in-memory database to a timestamped JSON file in
/// the Saves/ directory after every API call. Runs asynchronously on a background thread so it
/// does not block the response. Useful for debugging and post-mortem analysis.
/// </summary>
public class AutoSaveInterceptor(UserDataStore store, ILogger<AutoSaveInterceptor> logger) : Interceptor
{
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
private static readonly string SavesDirectory = Path.Combine(AppContext.BaseDirectory, "Saves");
/// <summary>
/// Intercepts gRPC calls to auto-save the user's database to disk after each request.
/// </summary>
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
TResponse response = await continuation(request, context);
long userId = context.GetUserId();
if (userId > 0 && store.TryGet(userId, out DarkUserMemoryDatabase userDb))
{
string[] parts = context.Method.Split('/', StringSplitOptions.RemoveEmptyEntries);
string methodSuffix = parts.Length >= 2 ? $"{parts[^2]}_{parts[^1]}" : context.Method.TrimStart('/').Replace('/', '_');
_ = Task.Run(() => SaveUser(userId, userDb, methodSuffix));
}
return response;
}
/// <summary>
/// Serializes and writes a user's database to a timestamped JSON file.
/// </summary>
private void SaveUser(long userId, DarkUserMemoryDatabase userDb, string methodSuffix)
{
try
{
Directory.CreateDirectory(SavesDirectory);
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
string filePath = Path.Combine(SavesDirectory, $"{userId}_{timestamp}_{methodSuffix}.json");
string json = JsonSerializer.Serialize(userDb, JsonOptions);
File.WriteAllText(filePath, json);
//logger.LogDebug("Auto-saved user {UserId} to {FilePath}", userId, filePath);
}
catch (Exception ex)
{
logger.LogError(ex, "AutoSaveInterceptor failed to save user {UserId}", userId);
}
}
}

View File

@@ -0,0 +1,28 @@
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace MariesWonderland.Interceptors;
/// <summary>
/// gRPC interceptor that appends standard response metadata to every call.
/// Currently adds the <c>x-apb-response-datetime</c> trailer with the server's UTC timestamp
/// in Unix milliseconds, which the client uses for time synchronisation.
/// </summary>
public class CommonHeaderInterceptor : Interceptor
{
/// <summary>
/// Runs after the service handler completes and appends the response-datetime trailer.
/// </summary>
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
TResponse response = await continuation(request, context);
Metadata trailers = context.ResponseTrailers;
trailers.Add("x-apb-response-datetime", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
return response;
}
}

View File

@@ -0,0 +1,112 @@
using Google.Protobuf.Collections;
using Grpc.Core;
using Grpc.Core.Interceptors;
using MariesWonderland.Data;
using MariesWonderland.Extensions;
using MariesWonderland.Proto.Data;
using MariesWonderland.Proto.User;
using System.Collections.Concurrent;
using System.Reflection;
namespace MariesWonderland.Interceptors;
/// <summary>
/// gRPC interceptor that automatically computes and attaches <c>DiffUserData</c> to every response
/// that declares the field. Takes a before-snapshot of the user's database prior to service execution,
/// then computes the delta after the service mutates state. This means individual services never need
/// to populate DiffUserData manually.
/// Special-cases the RegisterUser flow where no userId is available in request headers: extracts the
/// newly assigned userId from the response to perform a full-state diff against an empty baseline.
/// </summary>
public class DiffInterceptor(UserDataStore store, ILogger<DiffInterceptor> logger) : Interceptor
{
private static readonly ConcurrentDictionary<Type, PropertyInfo?> PropertyCache = new();
private static readonly ConcurrentDictionary<Type, PropertyInfo?> UserIdPropertyCache = new();
/// <summary>
/// Intercepts every unary gRPC call. If the response type has a DiffUserData map field,
/// snapshots user state before execution, runs the handler, then populates the diff.
/// </summary>
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
long userId = context.GetUserId();
PropertyInfo? diffProp = PropertyCache.GetOrAdd(typeof(TResponse), static t => t.GetProperty(nameof(AuthUserResponse.DiffUserData)));
// Response type has no DiffUserData property — pass through without snapshotting
if (diffProp is null)
{
return await continuation(request, context);
}
if (userId != 0)
{
// Normal path: userId is known from request headers — snapshot before, diff after
Dictionary<string, string> before = store.TryGet(userId, out DarkUserMemoryDatabase userDb)
? UserDataDiffBuilder.Snapshot(userDb)
: [];
TResponse response = await continuation(request, context);
try
{
if (diffProp.GetValue(response) is MapField<string, DiffData> mapField)
{
Dictionary<string, DiffData> delta = UserDataDiffBuilder.Delta(before, userDb);
foreach ((string key, DiffData value) in delta)
{
mapField[key] = value;
}
if (delta.Count > 0)
{
string[] names = [.. delta.Keys];
Array.Sort(names, StringComparer.Ordinal);
context.ResponseTrailers.Add("x-apb-update-user-data-names", string.Join(",", names));
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "DiffInterceptor failed to populate DiffUserData on {Method}", context.Method);
}
return response;
}
else
{
// RegisterUser path: userId=0 in headers because the user doesn't exist yet.
// Run the handler first, then extract the newly assigned userId from the response
// and diff against an empty baseline to send all initial state to the client.
TResponse response = await continuation(request, context);
try
{
PropertyInfo? userIdProp = UserIdPropertyCache.GetOrAdd(typeof(TResponse), static t => t.GetProperty("UserId"));
if (userIdProp?.GetValue(response) is long newUserId && newUserId != 0)
{
if (store.TryGet(newUserId, out DarkUserMemoryDatabase userDb))
{
// Only populate if the map is empty (service didn't set it manually)
if (diffProp.GetValue(response) is MapField<string, DiffData> mapField && mapField.Count == 0)
{
foreach ((string key, DiffData value) in UserDataDiffBuilder.Delta([], userDb))
{
mapField[key] = value;
}
}
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "DiffInterceptor failed to populate DiffUserData on {Method}", context.Method);
}
return response;
}
}
}

View File

@@ -0,0 +1,172 @@
using Google.Protobuf;
using Grpc.Core;
using Grpc.Core.Interceptors;
using MariesWonderland.Proto.Data;
using MariesWonderland.Proto.User;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace MariesWonderland.Interceptors;
/// <summary>
/// gRPC interceptor that logs every request/response pair. Writes compact JSON summaries to the
/// configured logger at Debug level and full indented payloads to timestamped files in the GRPC/
/// directory. Large fields (DiffUserData, UserDataJson, TableName) are stripped from log output
/// to keep console logs readable.
/// </summary>
public class LoggingInterceptor(ILogger<LoggingInterceptor> logger) : Interceptor
{
private static readonly List<string> ExcludedPropertyNames = [
nameof(AuthUserResponse.DiffUserData),
nameof(TableNameList.TableName),
nameof(UserDataGetResponse.UserDataJson)
];
private static readonly JsonSerializerOptions IndentedOptions = new() { WriteIndented = true };
private static readonly string GrpcDirectory = Path.Combine(AppContext.BaseDirectory, "GRPC");
/// <summary>
/// Intercepts gRPC calls to log request/response JSON and write full payloads to disk.
/// </summary>
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
string methodName = context.Method;
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug("[GRPC] >> {Method} (request)", methodName);
logger.LogDebug("{Json}", SerializeForLog(request));
}
TResponse response = await continuation(request, context);
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug("[GRPC] << {Method} (response)", methodName);
logger.LogDebug("{Json}", SerializeForLog(response));
}
_ = Task.Run(() => WriteToDisk(methodName, request, response));
return response;
}
/// <summary>
/// Writes a timestamped JSON file containing the full request and response for a gRPC call.
/// </summary>
private void WriteToDisk<TRequest, TResponse>(string methodName, TRequest request, TResponse response)
{
try
{
string[] parts = methodName.Split('/', StringSplitOptions.RemoveEmptyEntries);
string methodSuffix = parts.Length >= 2 ? $"{parts[^2]}_{parts[^1]}" : methodName.TrimStart('/').Replace('/', '_');
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss_fff");
string fileName = $"{timestamp}_{methodSuffix}.json";
Directory.CreateDirectory(GrpcDirectory);
string filePath = Path.Combine(GrpcDirectory, fileName);
StringBuilder sb = new();
sb.AppendLine("{");
sb.AppendLine($" \"method\": {JsonSerializer.Serialize(methodName)},");
sb.AppendLine($" \"timestamp\": \"{DateTime.UtcNow:O}\",");
sb.AppendLine($" \"request\": {SerializeFull(request)},");
sb.AppendLine($" \"response\": {SerializeFull(response)}");
sb.AppendLine("}");
File.WriteAllText(filePath, sb.ToString());
}
catch (Exception ex)
{
logger.LogError(ex, "LoggingInterceptor failed to write GRPC log for {Method}", methodName);
}
}
/// <summary>
/// Serializes an object to indented JSON, using protobuf JSON format for IMessage types.
/// </summary>
private static string SerializeFull(object? obj)
{
if (obj is null) return "null";
if (obj is IMessage message)
{
string json = JsonFormatter.Default.Format(message);
// Re-format with indentation for readability
try
{
JsonNode? node = JsonNode.Parse(json);
return node?.ToJsonString(IndentedOptions) ?? json;
}
catch { return json; }
}
return JsonSerializer.Serialize(obj, IndentedOptions);
}
/// <summary>
/// Serializes an object to compact JSON for log output, stripping excluded properties.
/// </summary>
private static string SerializeForLog(object obj)
{
string json = obj is IMessage message
? JsonFormatter.Default.Format(message)
: JsonSerializer.Serialize(obj);
return RemovePropertiesFromJson(json);
}
/// <summary>
/// Removes excluded property names from a JSON string.
/// </summary>
private static string RemovePropertiesFromJson(string json)
{
try
{
JsonNode? node = JsonNode.Parse(json);
if (node is null) return json;
RemoveProperties(node);
// Use compact JSON (no indentation) to match prior logging style.
return node.ToJsonString(new JsonSerializerOptions { WriteIndented = false });
}
catch
{
// If parsing fails for any reason, return the original JSON so logging still occurs.
return json;
}
}
/// <summary>
/// Recursively removes excluded properties from a JsonNode tree.
/// </summary>
private static void RemoveProperties(JsonNode? node)
{
if (node is JsonObject obj)
{
// Iterate over a snapshot of the keys because we'll be mutating the object.
foreach (var key in obj.Select(kvp => kvp.Key).ToList())
{
if (ExcludedPropertyNames.Contains(key, StringComparer.OrdinalIgnoreCase))
{
obj.Remove(key);
}
else
{
RemoveProperties(obj[key]);
}
}
}
else if (node is JsonArray arr)
{
foreach (var item in arr)
{
RemoveProperties(item);
}
}
// Primitives (JsonValue) do not contain nested properties; nothing to do.
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="proto\*.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<Content Update="Data\MasterData\**\*.json" CopyToOutputDirectory="PreserveNewest" />
<Content Update="Data\UserData\**\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
<PackageReference Include="MessagePack" Version="3.1.4" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@
using MessagePack;
using MessagePack.Formatters;
namespace MariesWonderland.MasterMemory;
/// <summary>
/// MessagePack resolver used exclusively to deserialize the binary database header
/// (<c>Dictionary&lt;string, (int, int)&gt;</c> mapping table names to data offsets).
/// </summary>
internal sealed class HeaderFormatterResolver : IFormatterResolver
{
public static readonly IFormatterResolver Instance = new HeaderFormatterResolver();
/// <summary>Pre-configured options using this resolver (no compression for header).</summary>
public static readonly MessagePackSerializerOptions StandardOptions =
MessagePackSerializerOptions.Standard.WithResolver(Instance);
private HeaderFormatterResolver() { }
/// <inheritdoc/>
public IMessagePackFormatter<T>? GetFormatter<T>()
{
if (typeof(T) == typeof(Dictionary<string, (int, int)>))
return (IMessagePackFormatter<T>)(object)new DictionaryFormatter<string, (int, int)>();
if (typeof(T) == typeof(string))
return (IMessagePackFormatter<T>)(object)NullableStringFormatter.Instance;
if (typeof(T) == typeof((int, int)))
return (IMessagePackFormatter<T>)(object)new IntIntValueTupleFormatter();
if (typeof(T) == typeof(int))
return (IMessagePackFormatter<T>)(object)Int32Formatter.Instance;
return null;
}
}

View File

@@ -0,0 +1,30 @@
using MessagePack;
using MessagePack.Formatters;
namespace MariesWonderland.MasterMemory;
/// <summary>
/// MessagePack formatter for <c>(int, int)</c> value tuples stored as 2-element arrays
/// in the master database binary header.
/// </summary>
internal sealed class IntIntValueTupleFormatter : IMessagePackFormatter<(int, int)>
{
public void Serialize(ref MessagePackWriter writer, (int, int) value, MessagePackSerializerOptions options)
{
writer.WriteArrayHeader(2);
writer.WriteInt32(value.Item1);
writer.WriteInt32(value.Item2);
}
public (int, int) Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
if (reader.IsNil)
throw new InvalidOperationException("Header tuple entry is nil.");
var count = reader.ReadArrayHeader();
if (count != 2)
throw new InvalidOperationException($"Expected 2-element tuple, got {count}.");
return (reader.ReadInt32(), reader.ReadInt32());
}
}

View File

@@ -0,0 +1,29 @@
using MessagePack;
using MessagePack.Formatters;
namespace MariesWonderland.MasterMemory;
/// <summary>
/// MessagePack resolver that interns deserialized strings to reduce memory usage from
/// repeated values (e.g. asset paths that appear across many master data records).
/// </summary>
internal sealed class InternStringResolver : IFormatterResolver, IMessagePackFormatter<string>
{
private readonly IFormatterResolver _inner;
public InternStringResolver(IFormatterResolver inner) => _inner = inner;
/// <inheritdoc/>
public IMessagePackFormatter<T>? GetFormatter<T>() => _inner.GetFormatter<T>();
/// <inheritdoc/>
public void Serialize(ref MessagePackWriter writer, string value, MessagePackSerializerOptions options)
=> throw new NotSupportedException();
/// <inheritdoc/>
public string? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
var value = reader.ReadString();
return value is not null ? string.Intern(value) : value;
}
}

View File

@@ -0,0 +1,33 @@
using System.Text;
namespace MariesWonderland.MasterMemory;
/// <summary>
/// Maps an EntityM* class name to its binary table key used in the master database header.
/// The table name is derived by converting the class name to snake_case and dropping the
/// leading "entity_" prefix (e.g. "EntityMCharacter" → "m_character").
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class MemoryTableAttribute : Attribute
{
/// <summary>The table key as it appears in the binary database header.</summary>
public string TableName { get; }
public MemoryTableAttribute(string tableName)
{
// "EntityMCharacter" → "entity_m_character" → split once on '_' → "m_character"
TableName = ToSnakeCase(tableName).Split('_', 2)[1];
}
private static string ToSnakeCase(string value)
{
var sb = new StringBuilder(value.Length + 10);
for (int i = 0; i < value.Length; i++)
{
if (i > 0 && char.IsUpper(value[i]))
sb.Append('_');
sb.Append(char.ToLowerInvariant(value[i]));
}
return sb.ToString();
}
}

View File

@@ -0,0 +1,26 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUser : IUserEntity
{
public long UserId { get; set; }
public long PlayerId { get; set; }
public int OsType { get; set; }
public PlatformType PlatformType { get; set; }
public int UserRestrictionType { get; set; }
public long RegisterDatetime { get; set; }
public long GameStartDatetime { get; set; }
public int BirthYear { get; set; }
public int BirthMonth { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,12 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserApple : IUserEntity
{
public long UserId { get; set; }
public string AppleId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserAutoSaleSettingDetail : IUserEntity
{
public long UserId { get; set; }
public int PossessionAutoSaleItemType { get; set; }
public string PossessionAutoSaleItemValue { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserBeginnerCampaign : IUserEntity
{
public long UserId { get; set; }
public int BeginnerCampaignId { get; set; }
public long CampaignRegisterDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserBigHuntMaxScore : IUserEntity
{
public long UserId { get; set; }
public int BigHuntBossId { get; set; }
public long MaxScore { get; set; }
public long MaxScoreUpdateDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserBigHuntProgressStatus : IUserEntity
{
public long UserId { get; set; }
public int CurrentBigHuntBossQuestId { get; set; }
public int CurrentBigHuntQuestId { get; set; }
public int CurrentQuestSceneId { get; set; }
public bool IsDryRun { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserBigHuntScheduleMaxScore : IUserEntity
{
public long UserId { get; set; }
public int BigHuntScheduleId { get; set; }
public int BigHuntBossId { get; set; }
public long MaxScore { get; set; }
public long MaxScoreUpdateDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserBigHuntStatus : IUserEntity
{
public long UserId { get; set; }
public int BigHuntBossQuestId { get; set; }
public int DailyChallengeCount { get; set; }
public long LatestChallengeDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserBigHuntWeeklyMaxScore : IUserEntity
{
public long UserId { get; set; }
public long BigHuntWeeklyVersion { get; set; }
public AttributeType AttributeType { get; set; }
public long MaxScore { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserBigHuntWeeklyStatus : IUserEntity
{
public long UserId { get; set; }
public long BigHuntWeeklyVersion { get; set; }
public bool IsReceivedWeeklyReward { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCageOrnamentReward : IUserEntity
{
public long UserId { get; set; }
public int CageOrnamentId { get; set; }
public long AcquisitionDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCharacter : IUserEntity
{
public long UserId { get; set; }
public int CharacterId { get; set; }
public int Level { get; set; }
public int Exp { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,20 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCharacterBoard : IUserEntity
{
public long UserId { get; set; }
public int CharacterBoardId { get; set; }
public int PanelReleaseBit1 { get; set; }
public int PanelReleaseBit2 { get; set; }
public int PanelReleaseBit3 { get; set; }
public int PanelReleaseBit4 { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCharacterBoardAbility : IUserEntity
{
public long UserId { get; set; }
public int CharacterId { get; set; }
public int AbilityId { get; set; }
public int Level { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,12 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCharacterBoardCompleteReward : IUserEntity
{
public long UserId { get; set; }
public int CharacterBoardCompleteRewardId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,26 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCharacterBoardStatusUp : IUserEntity
{
public long UserId { get; set; }
public int CharacterId { get; set; }
public StatusCalculationType StatusCalculationType { get; set; }
public int Hp { get; set; }
public int Attack { get; set; }
public int Vitality { get; set; }
public int Agility { get; set; }
public int CriticalRatio { get; set; }
public int CriticalAttack { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,26 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCharacterCostumeLevelBonus : IUserEntity
{
public long UserId { get; set; }
public int CharacterId { get; set; }
public StatusCalculationType StatusCalculationType { get; set; }
public int Hp { get; set; }
public int Attack { get; set; }
public int Vitality { get; set; }
public int Agility { get; set; }
public int CriticalRatio { get; set; }
public int CriticalAttack { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCharacterRebirth : IUserEntity
{
public long UserId { get; set; }
public int CharacterId { get; set; }
public int RebirthCount { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCharacterViewerField : IUserEntity
{
public long UserId { get; set; }
public int CharacterViewerFieldId { get; set; }
public long ReleaseDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserComebackCampaign : IUserEntity
{
public long UserId { get; set; }
public int ComebackCampaignId { get; set; }
public long ComebackDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,20 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCompanion : IUserEntity
{
public long UserId { get; set; }
public string UserCompanionUuid { get; set; }
public int CompanionId { get; set; }
public int HeadupDisplayViewId { get; set; }
public int Level { get; set; }
public long AcquisitionDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserConsumableItem : IUserEntity
{
public long UserId { get; set; }
public int ConsumableItemId { get; set; }
public int Count { get; set; }
public long FirstAcquisitionDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserContentsStory : IUserEntity
{
public long UserId { get; set; }
public int ContentsStoryId { get; set; }
public long PlayDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,26 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCostume : IUserEntity
{
public long UserId { get; set; }
public string UserCostumeUuid { get; set; }
public int CostumeId { get; set; }
public int LimitBreakCount { get; set; }
public int Level { get; set; }
public int Exp { get; set; }
public int HeadupDisplayViewId { get; set; }
public long AcquisitionDatetime { get; set; }
public int AwakenCount { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCostumeActiveSkill : IUserEntity
{
public long UserId { get; set; }
public string UserCostumeUuid { get; set; }
public int Level { get; set; }
public long AcquisitionDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,26 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCostumeAwakenStatusUp : IUserEntity
{
public long UserId { get; set; }
public string UserCostumeUuid { get; set; }
public StatusCalculationType StatusCalculationType { get; set; }
public int Hp { get; set; }
public int Attack { get; set; }
public int Vitality { get; set; }
public int Agility { get; set; }
public int CriticalRatio { get; set; }
public int CriticalAttack { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCostumeLevelBonusReleaseStatus : IUserEntity
{
public long UserId { get; set; }
public int CostumeId { get; set; }
public int LastReleasedBonusLevel { get; set; }
public int ConfirmedBonusLevel { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCostumeLotteryEffect : IUserEntity
{
public long UserId { get; set; }
public string UserCostumeUuid { get; set; }
public int SlotNumber { get; set; }
public int OddsNumber { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCostumeLotteryEffectAbility : IUserEntity
{
public long UserId { get; set; }
public string UserCostumeUuid { get; set; }
public int SlotNumber { get; set; }
public int AbilityId { get; set; }
public int AbilityLevel { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCostumeLotteryEffectPending : IUserEntity
{
public long UserId { get; set; }
public string UserCostumeUuid { get; set; }
public int SlotNumber { get; set; }
public int OddsNumber { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,26 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserCostumeLotteryEffectStatusUp : IUserEntity
{
public long UserId { get; set; }
public string UserCostumeUuid { get; set; }
public StatusCalculationType StatusCalculationType { get; set; }
public int Hp { get; set; }
public int Attack { get; set; }
public int Vitality { get; set; }
public int Agility { get; set; }
public int CriticalRatio { get; set; }
public int CriticalAttack { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,24 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserDeck : IUserEntity
{
public long UserId { get; set; }
public DeckType DeckType { get; set; }
public int UserDeckNumber { get; set; }
public string UserDeckCharacterUuid01 { get; set; }
public string UserDeckCharacterUuid02 { get; set; }
public string UserDeckCharacterUuid03 { get; set; }
public string Name { get; set; }
public int Power { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,22 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserDeckCharacter : IUserEntity
{
public long UserId { get; set; }
public string UserDeckCharacterUuid { get; set; }
public string UserCostumeUuid { get; set; }
public string MainUserWeaponUuid { get; set; }
public string UserCompanionUuid { get; set; }
public int Power { get; set; }
public string UserThoughtUuid { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserDeckCharacterDressupCostume : IUserEntity
{
public long UserId { get; set; }
public string UserDeckCharacterUuid { get; set; }
public int DressupCostumeId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserDeckLimitContentDeletedCharacter : IUserEntity
{
public long UserId { get; set; }
public int UserDeckNumber { get; set; }
public int UserDeckCharacterNumber { get; set; }
public int CostumeId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,20 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserDeckLimitContentRestricted : IUserEntity
{
public long UserId { get; set; }
public int EventQuestChapterId { get; set; }
public int QuestId { get; set; }
public string DeckRestrictedUuid { get; set; }
public PossessionType PossessionType { get; set; }
public string TargetUuid { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserDeckPartsGroup : IUserEntity
{
public long UserId { get; set; }
public string UserDeckCharacterUuid { get; set; }
public string UserPartsUuid { get; set; }
public int SortOrder { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace MariesWonderland.Models.Entities;
public class EntityIUserDeckSubWeaponGroup : IUserEntity
{
public long UserId { get; set; }
public string UserDeckCharacterUuid { get; set; }
public string UserWeaponUuid { get; set; }
public int SortOrder { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserDeckTypeNote : IUserEntity
{
public long UserId { get; set; }
public DeckType DeckType { get; set; }
public int MaxDeckPower { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserDokan : IUserEntity
{
public long UserId { get; set; }
public int DokanId { get; set; }
public long DisplayDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserEventQuestDailyGroupCompleteReward : IUserEntity
{
public long UserId { get; set; }
public int LastRewardReceiveEventQuestDailyGroupId { get; set; }
public long LastRewardReceiveDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserEventQuestGuerrillaFreeOpen : IUserEntity
{
public long UserId { get; set; }
public long StartDatetime { get; set; }
public int OpenMinutes { get; set; }
public int DailyOpenedCount { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserEventQuestLabyrinthSeason : IUserEntity
{
public long UserId { get; set; }
public int EventQuestChapterId { get; set; }
public int LastJoinSeasonNumber { get; set; }
public int LastSeasonRewardReceivedSeasonNumber { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserEventQuestLabyrinthStage : IUserEntity
{
public long UserId { get; set; }
public int EventQuestChapterId { get; set; }
public int StageOrder { get; set; }
public bool IsReceivedStageClearReward { get; set; }
public int AccumulationRewardReceivedQuestMissionCount { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserEventQuestProgressStatus : IUserEntity
{
public long UserId { get; set; }
public int CurrentEventQuestChapterId { get; set; }
public int CurrentQuestId { get; set; }
public int CurrentQuestSceneId { get; set; }
public int HeadQuestSceneId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserEventQuestTowerAccumulationReward : IUserEntity
{
public long UserId { get; set; }
public int EventQuestChapterId { get; set; }
public int LatestRewardReceiveQuestMissionClearCount { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserExplore : IUserEntity
{
public long UserId { get; set; }
public bool IsUseExploreTicket { get; set; }
public int PlayingExploreId { get; set; }
public long LatestPlayDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserExploreScore : IUserEntity
{
public long UserId { get; set; }
public int ExploreId { get; set; }
public int MaxScore { get; set; }
public long MaxScoreUpdateDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserExtraQuestProgressStatus : IUserEntity
{
public long UserId { get; set; }
public int CurrentQuestId { get; set; }
public int CurrentQuestSceneId { get; set; }
public int HeadQuestSceneId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,12 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserFacebook : IUserEntity
{
public long UserId { get; set; }
public long FacebookId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,12 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserGem : IUserEntity
{
public long UserId { get; set; }
public int PaidGem { get; set; }
public int FreeGem { get; set; }
}

View File

@@ -0,0 +1,20 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserGimmick : IUserEntity
{
public long UserId { get; set; }
public int GimmickSequenceScheduleId { get; set; }
public int GimmickSequenceId { get; set; }
public int GimmickId { get; set; }
public bool IsGimmickCleared { get; set; }
public long StartDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,22 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserGimmickOrnamentProgress : IUserEntity
{
public long UserId { get; set; }
public int GimmickSequenceScheduleId { get; set; }
public int GimmickSequenceId { get; set; }
public int GimmickId { get; set; }
public int GimmickOrnamentIndex { get; set; }
public int ProgressValueBit { get; set; }
public long BaseDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserGimmickSequence : IUserEntity
{
public long UserId { get; set; }
public int GimmickSequenceScheduleId { get; set; }
public int GimmickSequenceId { get; set; }
public bool IsGimmickSequenceCleared { get; set; }
public long ClearDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserGimmickUnlock : IUserEntity
{
public long UserId { get; set; }
public int GimmickSequenceScheduleId { get; set; }
public int GimmickSequenceId { get; set; }
public int GimmickId { get; set; }
public bool IsUnlocked { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserImportantItem : IUserEntity
{
public long UserId { get; set; }
public int ImportantItemId { get; set; }
public int Count { get; set; }
public long FirstAcquisitionDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserLimitedOpen : IUserEntity
{
public long UserId { get; set; }
public LimitedOpenTargetType LimitedOpenTargetType { get; set; }
public int TargetId { get; set; }
public long OpenDatetime { get; set; }
public long CloseDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,20 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserLogin : IUserEntity
{
public long UserId { get; set; }
public int TotalLoginCount { get; set; }
public int ContinualLoginCount { get; set; }
public int MaxContinualLoginCount { get; set; }
public long LastLoginDatetime { get; set; }
public long LastComebackLoginDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserLoginBonus : IUserEntity
{
public long UserId { get; set; }
public int LoginBonusId { get; set; }
public int CurrentPageNumber { get; set; }
public int CurrentStampNumber { get; set; }
public long LatestRewardReceiveDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,12 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMainQuestFlowStatus : IUserEntity
{
public long UserId { get; set; }
public QuestFlowType CurrentQuestFlowType { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMainQuestMainFlowStatus : IUserEntity
{
public long UserId { get; set; }
public int CurrentMainQuestRouteId { get; set; }
public int CurrentQuestSceneId { get; set; }
public int HeadQuestSceneId { get; set; }
public bool IsReachedLastQuestScene { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMainQuestProgressStatus : IUserEntity
{
public long UserId { get; set; }
public int CurrentQuestSceneId { get; set; }
public int HeadQuestSceneId { get; set; }
public QuestFlowType CurrentQuestFlowType { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMainQuestReplayFlowStatus : IUserEntity
{
public long UserId { get; set; }
public int CurrentHeadQuestSceneId { get; set; }
public int CurrentQuestSceneId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMainQuestSeasonRoute : IUserEntity
{
public long UserId { get; set; }
public int MainQuestSeasonId { get; set; }
public int MainQuestRouteId { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,16 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMaterial : IUserEntity
{
public long UserId { get; set; }
public int MaterialId { get; set; }
public int Count { get; set; }
public long FirstAcquisitionDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,20 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMission : IUserEntity
{
public long UserId { get; set; }
public int MissionId { get; set; }
public long StartDatetime { get; set; }
public int ProgressValue { get; set; }
public MissionProgressStatusType MissionProgressStatusType { get; set; }
public long ClearDatetime { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMissionCompletionProgress : IUserEntity
{
public long UserId { get; set; }
public int MissionId { get; set; }
public long ProgressValue { get; set; }
public long LatestVersion { get; set; }
}

View File

@@ -0,0 +1,18 @@
using MariesWonderland.Models.Type;
namespace MariesWonderland.Models.Entities;
public class EntityIUserMissionPassPoint : IUserEntity
{
public long UserId { get; set; }
public int MissionPassId { get; set; }
public int Point { get; set; }
public int PremiumRewardReceivedLevel { get; set; }
public int NoPremiumRewardReceivedLevel { get; set; }
public long LatestVersion { get; set; }
}

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