Initial commit

This commit is contained in:
thexeondev 2023-12-09 06:45:08 +03:00
commit 0f19985a17
267 changed files with 14383 additions and 0 deletions

398
.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,25 @@
using System.Collections.Immutable;
namespace FurinaImpact.Common.Constants;
public static class AvatarConstants
{
public static readonly ImmutableArray<string> CommonAbilities = ImmutableArray.Create
(
"Avatar_DefaultAbility_VisionReplaceDieInvincible",
"Avatar_DefaultAbility_AvartarInShaderChange",
"Avatar_SprintBS_Invincible",
"Avatar_Freeze_Duration_Reducer",
"Avatar_Attack_ReviveEnergy",
"Avatar_Component_Initializer",
"Avatar_FallAnthem_Achievement_Listener",
"GrapplingHookSkill_Ability",
"Avatar_PlayerBoy_DiveStamina_Reduction",
"Ability_Avatar_Dive_Team",
"Ability_Avatar_Dive_SealEcho",
"Absorb_SealEcho_Bullet_01",
"Absorb_SealEcho_Bullet_02",
"Ability_Avatar_Dive_CrabShield",
"ActivityAbility_Absorb_Shoot",
"SceneAbility_DiveVolume"
);
}

View file

@ -0,0 +1,100 @@
namespace FurinaImpact.Common.Constants;
public static class FightProp
{
public const uint FIGHT_PROP_NONE = 0;
public const uint FIGHT_PROP_BASE_HP = 1;
public const uint FIGHT_PROP_HP = 2;
public const uint FIGHT_PROP_HP_PERCENT = 3;
public const uint FIGHT_PROP_BASE_ATTACK = 4;
public const uint FIGHT_PROP_ATTACK = 5;
public const uint FIGHT_PROP_ATTACK_PERCENT = 6;
public const uint FIGHT_PROP_BASE_DEFENSE = 7;
public const uint FIGHT_PROP_DEFENSE = 8;
public const uint FIGHT_PROP_DEFENSE_PERCENT = 9;
public const uint FIGHT_PROP_BASE_SPEED = 10;
public const uint FIGHT_PROP_SPEED_PERCENT = 11;
public const uint FIGHT_PROP_HP_MP_PERCENT = 12;
public const uint FIGHT_PROP_ATTACK_MP_PERCENT = 13;
public const uint FIGHT_PROP_CRITICAL = 20;
public const uint FIGHT_PROP_ANTI_CRITICAL = 21;
public const uint FIGHT_PROP_CRITICAL_HURT = 22;
public const uint FIGHT_PROP_CHARGE_EFFICIENCY = 23;
public const uint FIGHT_PROP_ADD_HURT = 24;
public const uint FIGHT_PROP_SUB_HURT = 25;
public const uint FIGHT_PROP_HEAL_ADD = 26;
public const uint FIGHT_PROP_HEALED_ADD = 27;
public const uint FIGHT_PROP_ELEMENT_MASTERY = 28;
public const uint FIGHT_PROP_PHYSICAL_SUB_HURT = 29;
public const uint FIGHT_PROP_PHYSICAL_ADD_HURT = 30;
public const uint FIGHT_PROP_DEFENCE_IGNORE_RATIO = 31;
public const uint FIGHT_PROP_DEFENCE_IGNORE_DELTA = 32;
public const uint FIGHT_PROP_FIRE_ADD_HURT = 40;
public const uint FIGHT_PROP_ELEC_ADD_HURT = 41;
public const uint FIGHT_PROP_WATER_ADD_HURT = 42;
public const uint FIGHT_PROP_GRASS_ADD_HURT = 43;
public const uint FIGHT_PROP_WIND_ADD_HURT = 44;
public const uint FIGHT_PROP_ROCK_ADD_HURT = 45;
public const uint FIGHT_PROP_ICE_ADD_HURT = 46;
public const uint FIGHT_PROP_HIT_HEAD_ADD_HURT = 47;
public const uint FIGHT_PROP_FIRE_SUB_HURT = 50;
public const uint FIGHT_PROP_ELEC_SUB_HURT = 51;
public const uint FIGHT_PROP_WATER_SUB_HURT = 52;
public const uint FIGHT_PROP_GRASS_SUB_HURT = 53;
public const uint FIGHT_PROP_WIND_SUB_HURT = 54;
public const uint FIGHT_PROP_ROCK_SUB_HURT = 55;
public const uint FIGHT_PROP_ICE_SUB_HURT = 56;
public const uint FIGHT_PROP_EFFECT_HIT = 60;
public const uint FIGHT_PROP_EFFECT_RESIST = 61;
public const uint FIGHT_PROP_FREEZE_RESIST = 62;
public const uint FIGHT_PROP_TORPOR_RESIST = 63;
public const uint FIGHT_PROP_DIZZY_RESIST = 64;
public const uint FIGHT_PROP_FREEZE_SHORTEN = 65;
public const uint FIGHT_PROP_TORPOR_SHORTEN = 66;
public const uint FIGHT_PROP_DIZZY_SHORTEN = 67;
public const uint FIGHT_PROP_MAX_FIRE_ENERGY = 70;
public const uint FIGHT_PROP_MAX_ELEC_ENERGY = 71;
public const uint FIGHT_PROP_MAX_WATER_ENERGY = 72;
public const uint FIGHT_PROP_MAX_GRASS_ENERGY = 73;
public const uint FIGHT_PROP_MAX_WIND_ENERGY = 74;
public const uint FIGHT_PROP_MAX_ICE_ENERGY = 75;
public const uint FIGHT_PROP_MAX_ROCK_ENERGY = 76;
public const uint FIGHT_PROP_SKILL_CD_MINUS_RATIO = 80;
public const uint FIGHT_PROP_SHIELD_COST_MINUS_RATIO = 81;
public const uint FIGHT_PROP_CUR_FIRE_ENERGY = 1000;
public const uint FIGHT_PROP_CUR_ELEC_ENERGY = 1001;
public const uint FIGHT_PROP_CUR_WATER_ENERGY = 1002;
public const uint FIGHT_PROP_CUR_GRASS_ENERGY = 1003;
public const uint FIGHT_PROP_CUR_WIND_ENERGY = 1004;
public const uint FIGHT_PROP_CUR_ICE_ENERGY = 1005;
public const uint FIGHT_PROP_CUR_ROCK_ENERGY = 1006;
public const uint FIGHT_PROP_CUR_HP = 1010;
public const uint FIGHT_PROP_MAX_HP = 2000;
public const uint FIGHT_PROP_CUR_ATTACK = 2001;
public const uint FIGHT_PROP_CUR_DEFENSE = 2002;
public const uint FIGHT_PROP_CUR_SPEED = 2003;
public const uint FIGHT_PROP_NONEXTRA_ATTACK = 3000;
public const uint FIGHT_PROP_NONEXTRA_DEFENSE = 3001;
public const uint FIGHT_PROP_NONEXTRA_CRITICAL = 3002;
public const uint FIGHT_PROP_NONEXTRA_ANTI_CRITICAL = 3003;
public const uint FIGHT_PROP_NONEXTRA_CRITICAL_HURT = 3004;
public const uint FIGHT_PROP_NONEXTRA_CHARGE_EFFICIENCY = 3005;
public const uint FIGHT_PROP_NONEXTRA_ELEMENT_MASTERY = 3006;
public const uint FIGHT_PROP_NONEXTRA_PHYSICAL_SUB_HURT = 3007;
public const uint FIGHT_PROP_NONEXTRA_FIRE_ADD_HURT = 3008;
public const uint FIGHT_PROP_NONEXTRA_ELEC_ADD_HURT = 3009;
public const uint FIGHT_PROP_NONEXTRA_WATER_ADD_HURT = 3010;
public const uint FIGHT_PROP_NONEXTRA_GRASS_ADD_HURT = 3011;
public const uint FIGHT_PROP_NONEXTRA_WIND_ADD_HURT = 3012;
public const uint FIGHT_PROP_NONEXTRA_ROCK_ADD_HURT = 3013;
public const uint FIGHT_PROP_NONEXTRA_ICE_ADD_HURT = 3014;
public const uint FIGHT_PROP_NONEXTRA_FIRE_SUB_HURT = 3015;
public const uint FIGHT_PROP_NONEXTRA_ELEC_SUB_HURT = 3016;
public const uint FIGHT_PROP_NONEXTRA_WATER_SUB_HURT = 3017;
public const uint FIGHT_PROP_NONEXTRA_GRASS_SUB_HURT = 3018;
public const uint FIGHT_PROP_NONEXTRA_WIND_SUB_HURT = 3019;
public const uint FIGHT_PROP_NONEXTRA_ROCK_SUB_HURT = 3020;
public const uint FIGHT_PROP_NONEXTRA_ICE_SUB_HURT = 3021;
public const uint FIGHT_PROP_NONEXTRA_SKILL_CD_MINUS_RATIO = 3022;
public const uint FIGHT_PROP_NONEXTRA_SHIELD_COST_MINUS_RATIO = 3023;
public const uint FIGHT_PROP_NONEXTRA_PHYSICAL_ADD_HURT = 3024;
}

View file

@ -0,0 +1,49 @@
namespace FurinaImpact.Common.Constants;
public static class PlayerProp
{
public const uint PROP_NONE = 0;
public const uint PROP_EXP = 1001;
public const uint PROP_BREAK_LEVEL = 1002;
public const uint PROP_SATIATION_VAL = 1003;
public const uint PROP_SATIATION_PENALTY_TIME = 1004;
public const uint PROP_LEVEL = 4001;
public const uint PROP_LAST_CHANGE_AVATAR_TIME = 10001;
public const uint PROP_MAX_SPRING_VOLUME = 10002;
public const uint PROP_CUR_SPRING_VOLUME = 10003;
public const uint PROP_IS_SPRING_AUTO_USE = 10004;
public const uint PROP_SPRING_AUTO_USE_PERCENT = 10005;
public const uint PROP_IS_FLYABLE = 10006;
public const uint PROP_IS_WEATHER_LOCKED = 10007;
public const uint PROP_IS_GAME_TIME_LOCKED = 10008;
public const uint PROP_IS_TRANSFERABLE = 10009;
public const uint PROP_MAX_STAMINA = 10010;
public const uint PROP_CUR_PERSIST_STAMINA = 10011;
public const uint PROP_CUR_TEMPORARY_STAMINA = 10012;
public const uint PROP_PLAYER_LEVEL = 10013;
public const uint PROP_PLAYER_EXP = 10014;
public const uint PROP_PLAYER_HCOIN = 10015;
public const uint PROP_PLAYER_SCOIN = 10016;
public const uint PROP_PLAYER_MP_SETTING_TYPE = 10017;
public const uint PROP_IS_MP_MODE_AVAILABLE = 10018;
public const uint PROP_PLAYER_WORLD_LEVEL = 10019;
public const uint PROP_PLAYER_RESIN = 10020;
public const uint PROP_PLAYER_WAIT_SUB_HCOIN = 10022;
public const uint PROP_PLAYER_WAIT_SUB_SCOIN = 10023;
public const uint PROP_IS_ONLY_MP_WITH_PS_PLAYER = 10024;
public const uint PROP_PLAYER_MCOIN = 10025;
public const uint PROP_PLAYER_WAIT_SUB_MCOIN = 10026;
public const uint PROP_PLAYER_LEGENDARY_KEY = 10027;
public const uint PROP_IS_HAS_FIRST_SHARE = 10028;
public const uint PROP_PLAYER_FORGE_POINT = 10029;
public const uint PROP_CUR_CLIMATE_METER = 10035;
public const uint PROP_CUR_CLIMATE_TYPE = 10036;
public const uint PROP_CUR_CLIMATE_AREA_ID = 10037;
public const uint PROP_CUR_CLIMATE_AREA_CLIMATE_TYPE = 10038;
public const uint PROP_PLAYER_WORLD_LEVEL_LIMIT = 10039;
public const uint PROP_PLAYER_WORLD_LEVEL_ADJUST_CD = 10040;
public const uint PROP_PLAYER_LEGENDARY_DAILY_TASK_NUM = 10041;
public const uint PROP_PLAYER_HOME_COIN = 10042;
public const uint PROP_PLAYER_WAIT_SUB_HOME_COIN = 10043;
}

View file

@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace FurinaImpact.Common.Data.Binout.Ability;
public class AbilityData
{
[JsonPropertyName("abilityID")]
public required string AbilityId { get; set; }
[JsonPropertyName("abilityName")]
public required string AbilityName { get; set; }
[JsonPropertyName("abilityOverride")]
public required string AbilityOverride { get; set; }
public string GetAbilityOverride()
=> string.IsNullOrEmpty(AbilityOverride) ? "Default" : AbilityOverride;
}

View file

@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using FurinaImpact.Common.Data.Binout.Ability;
namespace FurinaImpact.Common.Data.Binout;
public class AvatarConfig
{
[JsonPropertyName("abilities")]
public List<AbilityData> Abilities { get; set; }
public AvatarConfig()
{
Abilities = new();
}
}

View file

@ -0,0 +1,50 @@
using System.Collections.Immutable;
using System.Text.Json;
using FurinaImpact.Common.Data.Provider;
using Microsoft.Extensions.Logging;
namespace FurinaImpact.Common.Data.Binout;
public class BinDataCollection
{
private readonly ImmutableDictionary<uint, AvatarConfig> _avatarConfigs;
public BinDataCollection(IAssetProvider assetProvider, ILogger<BinDataCollection> logger, DataHelper dataHelper)
{
_avatarConfigs = LoadAvatarConfigs(assetProvider, dataHelper);
logger.LogInformation("Loaded {count} avatar configs", _avatarConfigs.Count);
}
public AvatarConfig GetAvatarConfig(uint id)
{
return _avatarConfigs[id];
}
private static ImmutableDictionary<uint, AvatarConfig> LoadAvatarConfigs(IAssetProvider assetProvider, DataHelper dataHelper)
{
ImmutableDictionary<uint, AvatarConfig>.Builder builder = ImmutableDictionary.CreateBuilder<uint, AvatarConfig>();
IEnumerable<string> avatarConfigFiles = assetProvider.EnumerateAvatarConfigFiles();
foreach (string avatarConfigFile in avatarConfigFiles)
{
string avatarName = avatarConfigFile[(avatarConfigFile.LastIndexOf('_') + 1)..];
avatarName = avatarName.Remove(avatarName.IndexOf('.'));
if (dataHelper.TryResolveAvatarIdByName(avatarName, out uint id))
{
JsonDocument configJson = assetProvider.GetFileAsJsonDocument(avatarConfigFile);
if (configJson.RootElement.ValueKind != JsonValueKind.Object)
throw new JsonException($"BinDataCollection::LoadAvatarConfigs - expected an object, got {configJson.RootElement.ValueKind}");
AvatarConfig avatarConfig = configJson.RootElement.Deserialize<AvatarConfig>()!;
builder.Add(id, avatarConfig);
}
else
{
throw new KeyNotFoundException($"BinDataCollection::LoadAvatarConfigs - failed to resolve avatar id for {avatarName}");
}
}
return builder.ToImmutable();
}
}

View file

@ -0,0 +1,34 @@
using System.Collections.Immutable;
using FurinaImpact.Common.Data.Excel;
namespace FurinaImpact.Common.Data;
public class DataHelper
{
private readonly ImmutableDictionary<string, uint> _avatarNameToIdTable;
public DataHelper(ExcelTableCollection excelTables)
{
_avatarNameToIdTable = BuildAvatarNameToIdTable(excelTables);
}
public bool TryResolveAvatarIdByName(string avatarName, out uint id)
{
return _avatarNameToIdTable.TryGetValue(avatarName, out id);
}
private static ImmutableDictionary<string, uint> BuildAvatarNameToIdTable(ExcelTableCollection excelTables)
{
ImmutableDictionary<string, uint>.Builder builder = ImmutableDictionary.CreateBuilder<string, uint>();
ExcelTable avatarTable = excelTables.GetTable(ExcelType.Avatar);
for (int i = 0; i < avatarTable.Count; i++)
{
AvatarExcel excel = avatarTable.GetItemAt<AvatarExcel>(i);
string avatarName = excel.IconName[(excel.IconName.LastIndexOf('_') + 1)..];
builder.TryAdd(avatarName, excel.Id);
}
return builder.ToImmutable();
}
}

View file

@ -0,0 +1,16 @@
using FurinaImpact.Common.Data.Excel;
namespace FurinaImpact.Common.Data.Excel.Attributes;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class ExcelAttribute : Attribute
{
public ExcelType Type { get; }
public string AssetName { get; }
public ExcelAttribute(ExcelType type, string assetName)
{
Type = type;
AssetName = assetName;
}
}

View file

@ -0,0 +1,136 @@
using System.Text.Json.Serialization;
using FurinaImpact.Common.Data.Excel.Attributes;
namespace FurinaImpact.Common.Data.Excel;
[Excel(ExcelType.Avatar, "AvatarExcelConfigData.json")]
public class AvatarExcel : ExcelItem
{
// TODO: implement enums for some fields!
public override uint ExcelId => Id;
[JsonPropertyName("attackBase")]
public double AttackBase { get; set; }
[JsonPropertyName("avatarIdentityType")]
public string? AvatarIdentityType { get; set; }
[JsonPropertyName("avatarPromoteId")]
public uint AvatarPromoteId { get; set; }
[JsonPropertyName("avatarPromoteRewardIdList")]
public List<int> AvatarPromoteRewardIdList { get; set; }
[JsonPropertyName("avatarPromoteRewardLevelList")]
public List<int> AvatarPromoteRewardLevelList { get; set; }
[JsonPropertyName("bodyType")]
public required string BodyType { get; set; }
[JsonPropertyName("iconName")]
public required string IconName { get; set; }
[JsonPropertyName("chargeEfficiency")]
public double ChargeEfficiency { get; set; }
[JsonPropertyName("combatConfigHashSuffix")]
public ulong CombatConfigHashSuffix { get; set; }
[JsonPropertyName("controllerPathHashSuffix")]
public ulong ControllerPathHashSuffix { get; set; }
[JsonPropertyName("controllerPathRemoteHashSuffix")]
public ulong ControllerPathRemoteHashSuffix { get; set; }
[JsonPropertyName("coopPicNameHashSuffix")]
public ulong CoopPicNameHashSuffix { get; set; }
[JsonPropertyName("critical")]
public double Critical { get; set; }
[JsonPropertyName("criticalHurt")]
public double CriticalHurt { get; set; }
[JsonPropertyName("defenseBase")]
public double DefenseBase { get; set; }
[JsonPropertyName("descTextMapHash")]
public ulong DescTextMapHash { get; set; }
[JsonPropertyName("featureTagGroupId")]
public uint FeatureTagGroupId { get; set; }
[JsonPropertyName("gachaCardNameHashSuffix")]
public ulong GachaCardNameHashSuffix { get; set; }
[JsonPropertyName("gachaImageNameHashSuffix")]
public ulong GachaImageNameHashSuffix { get; set; }
[JsonPropertyName("hpBase")]
public double HpBase { get; set; }
[JsonPropertyName("id")]
public uint Id { get; set; }
[JsonPropertyName("infoDesc")]
public long InfoDesc { get; set; }
[JsonPropertyName("initialWeapon")]
public uint InitialWeapon { get; set; }
[JsonPropertyName("manekinJsonConfigHashSuffix")]
public long ManekinJsonConfigHashSuffix { get; set; }
[JsonPropertyName("manekinMotionConfig")]
public uint ManekinMotionConfig { get; set; }
[JsonPropertyName("manekinPathHashSuffix")]
public ulong ManekinPathHashSuffix { get; set; }
[JsonPropertyName("nameTextMapHash")]
public ulong NameTextMapHash { get; set; }
[JsonPropertyName("prefabPathHashSuffix")]
public ulong PrefabPathHashSuffix { get; set; }
[JsonPropertyName("prefabPathRagdollHashSuffix")]
public ulong PrefabPathRagdollHashSuffix { get; set; }
[JsonPropertyName("prefabPathRemoteHashSuffix")]
public ulong PrefabPathRemoteHashSuffix { get; set; }
[JsonPropertyName("propGrowCurves")]
public List<PropGrowCurve> PropGrowCurves { get; set; }
[JsonPropertyName("qualityType")]
public required string QualityType { get; set; }
[JsonPropertyName("skillDepotId")]
public uint SkillDepotId { get; set; }
[JsonPropertyName("staminaRecoverSpeed")]
public double StaminaRecoverSpeed { get; set; }
[JsonPropertyName("useType")]
public string? UseType { get; set; }
[JsonPropertyName("weaponType")]
public required string WeaponType { get; set; }
public AvatarExcel()
{
AvatarPromoteRewardIdList = new();
AvatarPromoteRewardLevelList = new();
PropGrowCurves = new();
}
}
public class PropGrowCurve
{
[JsonPropertyName("growCurve")]
public required string GrowCurve { get; set; }
[JsonPropertyName("type")]
public required string Type { get; set; }
}

View file

@ -0,0 +1,5 @@
namespace FurinaImpact.Common.Data.Excel;
public abstract class ExcelItem
{
public abstract uint ExcelId { get; }
}

View file

@ -0,0 +1,46 @@
using System.Collections.Immutable;
using System.Text.Json;
namespace FurinaImpact.Common.Data.Excel;
public class ExcelTable
{
public int Count => _items.Length;
private readonly ImmutableArray<ExcelItem> _items;
public ExcelTable(JsonDocument document, Type type)
{
_items = LoadData(document, type);
}
private static ImmutableArray<ExcelItem> LoadData(JsonDocument document, Type type)
{
ImmutableArray<ExcelItem>.Builder items = ImmutableArray.CreateBuilder<ExcelItem>();
foreach (JsonElement element in document.RootElement.EnumerateArray())
{
if (element.ValueKind != JsonValueKind.Object)
throw new ArgumentException($"ExcelTable::LoadData - expected an object, got {element.ValueKind}");
ExcelItem deserialized = (element.Deserialize(type) as ExcelItem)!;
items.Add(deserialized);
}
return items.ToImmutable();
}
public TExcel GetItemAt<TExcel>(int index) where TExcel : ExcelItem
{
return (_items[index] as TExcel)!;
}
public TExcel? GetItemById<TExcel>(uint id) where TExcel : ExcelItem
{
foreach (ExcelItem item in _items)
{
if (item.ExcelId == id)
return item as TExcel;
}
return null;
}
}

View file

@ -0,0 +1,41 @@
using System.Collections.Immutable;
using System.Reflection;
using System.Text.Json;
using FurinaImpact.Common.Data.Excel.Attributes;
using FurinaImpact.Common.Data.Provider;
using Microsoft.Extensions.Logging;
namespace FurinaImpact.Common.Data.Excel;
public class ExcelTableCollection
{
private readonly ImmutableDictionary<ExcelType, ExcelTable> _tables;
public ExcelTableCollection(IAssetProvider assetProvider, ILogger<ExcelTableCollection> logger)
{
_tables = LoadTables(assetProvider);
logger.LogInformation("Loaded {count} excel tables", _tables.Count);
}
public TExcel? GetExcel<TExcel>(ExcelType type, uint id) where TExcel : ExcelItem
=> _tables[type].GetItemById<TExcel>(id);
public ExcelTable GetTable(ExcelType type) => _tables[type];
private static ImmutableDictionary<ExcelType, ExcelTable> LoadTables(IAssetProvider assetProvider)
{
ImmutableDictionary<ExcelType, ExcelTable>.Builder tables = ImmutableDictionary.CreateBuilder<ExcelType, ExcelTable>();
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => type.GetCustomAttribute<ExcelAttribute>() != null);
foreach (Type type in types)
{
ExcelAttribute attribute = type.GetCustomAttribute<ExcelAttribute>()!;
JsonDocument tableJson = assetProvider.GetExcelTableJson(attribute.AssetName);
tables.Add(attribute.Type, new ExcelTable(tableJson, type));
}
return tables.ToImmutable();
}
}

View file

@ -0,0 +1,5 @@
namespace FurinaImpact.Common.Data.Excel;
public enum ExcelType
{
Avatar
}

View file

@ -0,0 +1,9 @@
using System.Text.Json;
namespace FurinaImpact.Common.Data.Provider;
public interface IAssetProvider
{
JsonDocument GetExcelTableJson(string assetName);
IEnumerable<string> EnumerateAvatarConfigFiles();
JsonDocument GetFileAsJsonDocument(string fullPath);
}

View file

@ -0,0 +1,27 @@
using System.Text.Json;
namespace FurinaImpact.Common.Data.Provider;
internal sealed class LocalAssetProvider : IAssetProvider
{
private const string ExcelDirectory = "assets/excel/";
private const string AvatarConfigDirectory = "assets/binout/avatar/";
public IEnumerable<string> EnumerateAvatarConfigFiles()
{
return Directory.GetFiles(AvatarConfigDirectory);
}
public JsonDocument GetFileAsJsonDocument(string fullPath)
{
using FileStream fileStream = new(fullPath, FileMode.Open, FileAccess.Read);
return JsonDocument.Parse(fileStream);
}
public JsonDocument GetExcelTableJson(string assetName)
{
string filePath = string.Concat(ExcelDirectory, assetName);
using FileStream fileStream = new(filePath, FileMode.Open, FileAccess.Read);
return JsonDocument.Parse(fileStream);
}
}

View file

@ -0,0 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
namespace FurinaImpact.Common.Data.Provider;
public static class ServiceCollectionExtensions
{
public static IServiceCollection UseLocalAssets(this IServiceCollection services)
{
return services.AddSingleton<IAssetProvider, LocalAssetProvider>();
}
}

View file

@ -0,0 +1,14 @@
namespace FurinaImpact.Common.Extensions;
public static class StringExtensions
{
public static uint GetStableHash(this string str)
{
uint hash = 0;
for (int i = 0; i < str.Length; i++)
{
hash = ((str[i] + 131 * hash) & 0xFFFFFFFF) >> 0;
}
return hash;
}
}

View file

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FurinaImpact.Protocol\FurinaImpact.Protocol.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="assets\security\client_public_key.der">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="assets\security\initial_key.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="assets\security\initial_key.ec2b">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,79 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using System.Text;
using FurinaImpact.Security.Util;
namespace FurinaImpact.Common.Security;
public static class MhySecurity
{
public static byte[] InitialKey { get; }
public static byte[] InitialKeyEc2b { get; }
public static byte[] RSAClientPublicKey { get; }
static MhySecurity()
{
InitialKey = File.ReadAllBytes("assets/security/initial_key.bin");
InitialKeyEc2b = File.ReadAllBytes("assets/security/initial_key.ec2b");
RSAClientPublicKey = File.ReadAllBytes("assets/security/client_public_key.der");
}
public static byte[] GenerateSecretKey(ulong seed)
{
byte[] key = GC.AllocateUninitializedArray<byte>(0x1000);
Span<byte> keySpan = key.AsSpan();
MT19937 mt = new(seed);
mt.Int63();
for (int i = 0; i < 0x1000; i += 8)
{
BinaryPrimitives.WriteUInt64BigEndian(keySpan[i..], mt.Int63());
}
return key;
}
public static byte[] EncryptWithRSA(ReadOnlySpan<byte> data)
{
using RSA cipher = RSA.Create();
cipher.ImportSubjectPublicKeyInfo(RSAClientPublicKey, out _);
const int chunkSize = 256 - 11;
int dataLength = data.Length;
int numChunks = dataLength / chunkSize;
if ((dataLength - (chunkSize * numChunks)) % chunkSize != 0) ++numChunks;
if (numChunks < 2)
{
return cipher.Encrypt(data, RSAEncryptionPadding.Pkcs1);
}
using MemoryStream stream = new();
for (int i = 0; i < numChunks; i++)
{
ReadOnlySpan<byte> chunk = data.Slice(i * chunkSize, Math.Min(dataLength, chunkSize));
stream.Write(cipher.Encrypt(chunk, RSAEncryptionPadding.Pkcs1));
dataLength -= chunkSize;
}
return stream.ToArray();
}
public static byte[] Xor(string data, ReadOnlySpan<byte> key)
{
byte[] result = Encoding.UTF8.GetBytes(data);
Xor(result, key);
return result;
}
public static void Xor(Span<byte> data, ReadOnlySpan<byte> key)
{
for (int i = 0; i < data.Length; i++)
{
data[i] ^= key[i % key.Length];
}
}
}

View file

@ -0,0 +1,68 @@
namespace FurinaImpact.Security.Util;
internal class MT19937
{
private const ulong N = 312;
private const ulong M = 156;
private const ulong MATRIX_A = 0xB5026F5AA96619E9L;
private const ulong UPPER_MASK = 0xFFFFFFFF80000000;
private const ulong LOWER_MASK = 0X7FFFFFFFUL;
private readonly ulong[] _mt = new ulong[N + 1];
private ulong _mti = N + 1;
public MT19937(ulong seed)
{
this.Seed(seed);
}
public void Seed(ulong seed)
{
_mt[0] = seed;
for (_mti = 1; _mti < N; _mti++)
{
_mt[_mti] = (6364136223846793005L * (_mt[_mti - 1] ^ (_mt[_mti - 1] >> 62)) + _mti);
}
}
public ulong Int63()
{
ulong x = 0;
ulong[] mag01 = new ulong[2] { 0x0UL, MATRIX_A };
if (_mti >= N)
{
ulong kk;
if (_mti == N + 1)
{
Seed(5489UL);
}
for (kk = 0; kk < (N - M); kk++)
{
x = (_mt[kk] & UPPER_MASK) | (_mt[kk + 1] & LOWER_MASK);
_mt[kk] = _mt[kk + M] ^ (x >> 1) ^ mag01[x & 0x1UL];
}
for (; kk < N - 1; kk++)
{
x = (_mt[kk] & UPPER_MASK) | (_mt[kk + 1] & LOWER_MASK);
_mt[kk] = _mt[kk - M] ^ (x >> 1) ^ mag01[x & 0x1UL];
}
x = (_mt[N - 1] & UPPER_MASK) | (_mt[0] & LOWER_MASK);
_mt[N - 1] = _mt[M - 1] ^ (x >> 1) ^ mag01[x & 0x1UL];
_mti = 0;
}
x = _mt[_mti++];
x ^= (x >> 29) & 0x5555555555555555L;
x ^= (x << 17) & 0x71D67FFFEDA60000L;
x ^= (x << 37) & 0xFFF7EEE000000000L;
x ^= (x >> 43);
return x;
}
public ulong IntN(ulong value)
{
return Int63() % value;
}
}

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,152 @@
using FurinaImpact.Common.Constants;
using FurinaImpact.Common.Security;
using FurinaImpact.Gameserver.Controllers.Attributes;
using FurinaImpact.Gameserver.Controllers.Result;
using FurinaImpact.Gameserver.Game;
using FurinaImpact.Gameserver.Game.Avatar;
using FurinaImpact.Gameserver.Game.Scene;
using FurinaImpact.Gameserver.Network.Session;
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Controllers;
[NetController]
internal class AccountController : ControllerBase
{
[NetCommand(CmdType.GetPlayerTokenReq)]
public ValueTask<IResult> OnGetPlayerTokenReq()
{
return ValueTask.FromResult(Response(CmdType.GetPlayerTokenRsp, new GetPlayerTokenRsp
{
ServerRandKey = Convert.ToBase64String(MhySecurity.EncryptWithRSA(new byte[8])),
Sign = string.Empty, // bypassed
Uid = 1337,
CountryCode = "RU",
PlatformType = 3
}));
}
[NetCommand(CmdType.PingReq)]
public ValueTask<IResult> OnPingReq()
{
return ValueTask.FromResult(Response(CmdType.PingRsp, new PingRsp
{
ServerTime = (uint)DateTimeOffset.Now.ToUnixTimeSeconds()
}));
}
[NetCommand(CmdType.PlayerLoginReq)]
public async ValueTask<IResult> OnPlayerLoginReq(NetSession session, Player player, SceneManager sceneManager)
{
player.InitDefaultPlayer();
await session.NotifyAsync(CmdType.PlayerDataNotify, new PlayerDataNotify
{
NickName = player.Name,
PropMap =
{
{PlayerProp.PROP_PLAYER_LEVEL, new() { Type = PlayerProp.PROP_PLAYER_LEVEL, Ival = 5 } },
{PlayerProp.PROP_IS_FLYABLE, new() { Type = PlayerProp.PROP_IS_FLYABLE, Ival = 1 } },
{PlayerProp.PROP_MAX_STAMINA, new() { Type = PlayerProp.PROP_MAX_STAMINA, Ival = 10000 } },
{PlayerProp.PROP_CUR_PERSIST_STAMINA, new() { Type = PlayerProp.PROP_CUR_PERSIST_STAMINA, Ival = 10000 } },
{PlayerProp.PROP_IS_TRANSFERABLE, new() { Type = PlayerProp.PROP_IS_TRANSFERABLE, Ival = 1 } },
{PlayerProp.PROP_IS_SPRING_AUTO_USE, new() { Type = PlayerProp.PROP_IS_SPRING_AUTO_USE, Ival = 1 } },
{PlayerProp.PROP_SPRING_AUTO_USE_PERCENT, new() { Type = PlayerProp.PROP_SPRING_AUTO_USE_PERCENT, Ival = 50 } }
}
});
AvatarDataNotify avatarDataNotify = new()
{
CurAvatarTeamId = player.CurTeamIndex,
ChooseAvatarGuid = 228
};
foreach (GameAvatar gameAvatar in player.Avatars)
{
avatarDataNotify.AvatarList.Add(gameAvatar.AsAvatarInfo());
}
foreach (GameAvatarTeam team in player.AvatarTeams)
{
AvatarTeam avatarTeam = new();
avatarTeam.AvatarGuidList.AddRange(team.AvatarGuidList);
avatarDataNotify.AvatarTeamMap.Add(team.Index, avatarTeam);
}
await session.NotifyAsync(CmdType.AvatarDataNotify, avatarDataNotify);
await session.NotifyAsync(CmdType.OpenStateUpdateNotify, new OpenStateUpdateNotify
{
OpenStateMap =
{
{1, 1},
{2, 1},
{3, 1},
{4, 1},
{5, 1},
{6, 1},
{7, 0},
{8, 1},
{10, 1},
{11, 1},
{12, 1},
{13, 1},
{14, 1},
{15, 1},
{27, 1},
{28, 1},
{29, 1},
{30, 1},
{31, 1},
{32, 1},
{33, 1},
{37, 1},
{38, 1},
{45, 1},
{47, 1},
{53, 1},
{54, 1},
{55, 1},
{59, 1},
{62, 1},
{65, 1},
{900, 1},
{901, 1},
{902, 1},
{903, 1},
{1001, 1},
{1002, 1},
{1003, 1},
{1004, 1},
{1005, 1},
{1007, 1},
{1008, 1},
{1009, 1},
{1010, 1},
{1100, 1},
{1103, 1},
{1300, 1},
{1401, 1},
{1403, 1},
{1700, 1},
{2100, 1},
{2101, 1},
{2103, 1},
{2400, 1},
{3701, 1},
{3702, 1},
{4100, 1 }
}
});
await sceneManager.EnterSceneAsync(3);
return Response(CmdType.PlayerLoginRsp, new PlayerLoginRsp
{
CountryCode = "RU",
GameBiz = "hk4e_global",
ResVersionConfig = new()
});
}
}

View file

@ -0,0 +1,14 @@
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Controllers.Attributes;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
internal class NetCommandAttribute : Attribute
{
public CmdType CmdType { get; }
public NetCommandAttribute(CmdType cmdType)
{
CmdType = cmdType;
}
}

View file

@ -0,0 +1,6 @@
namespace FurinaImpact.Gameserver.Controllers.Attributes;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class NetControllerAttribute : Attribute
{
}

View file

@ -0,0 +1,35 @@
using FurinaImpact.Gameserver.Controllers.Attributes;
using FurinaImpact.Gameserver.Controllers.Result;
using FurinaImpact.Gameserver.Game.Scene;
using FurinaImpact.Gameserver.Network.Session;
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Controllers;
[NetController]
internal class AvatarController : ControllerBase
{
[NetCommand(CmdType.SetUpAvatarTeamReq)]
public async ValueTask<IResult> OnSetUpAvatarTeamReq(NetSession session, SceneManager sceneManager)
{
SetUpAvatarTeamReq request = Packet!.DecodeBody<SetUpAvatarTeamReq>();
AvatarTeam newTeam = new();
newTeam.AvatarGuidList.AddRange(request.AvatarTeamGuidList);
await session.NotifyAsync(CmdType.AvatarTeamUpdateNotify, new AvatarTeamUpdateNotify
{
AvatarTeamMap = { { request.TeamId, newTeam } }
});
await sceneManager.ChangeTeamAvatarsAsync(request.AvatarTeamGuidList.ToArray());
SetUpAvatarTeamRsp response = new()
{
CurAvatarGuid = request.CurAvatarGuid,
TeamId = request.TeamId,
};
response.AvatarTeamGuidList.AddRange(request.AvatarTeamGuidList);
return Response(CmdType.SetUpAvatarTeamRsp, response);
}
}

View file

@ -0,0 +1,25 @@
using FurinaImpact.Gameserver.Controllers.Result;
using FurinaImpact.Gameserver.Network;
using FurinaImpact.Protocol;
using Google.Protobuf;
namespace FurinaImpact.Gameserver.Controllers;
internal abstract class ControllerBase
{
public NetPacket? Packet { get; set; }
protected IResult Ok()
{
return new SinglePacketResult(null);
}
protected IResult Response<TMessage>(CmdType cmdType, TMessage message) where TMessage : IMessage
{
return new SinglePacketResult(new()
{
CmdType = cmdType,
Head = Memory<byte>.Empty,
Body = message.ToByteArray()
});
}
}

View file

@ -0,0 +1,105 @@
using System.Collections.Immutable;
using System.Linq.Expressions;
using System.Reflection;
using FurinaImpact.Gameserver.Controllers.Attributes;
using FurinaImpact.Gameserver.Controllers.Result;
using FurinaImpact.Gameserver.Network;
using FurinaImpact.Protocol;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace FurinaImpact.Gameserver.Controllers.Dispatching;
internal class NetCommandDispatcher
{
private static readonly ImmutableDictionary<CmdType, Func<IServiceProvider, NetPacket, ValueTask<IResult>>> _handlers;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
static NetCommandDispatcher()
{
_handlers = InitializeHandlers();
}
public NetCommandDispatcher(IServiceProvider serviceProvider, ILogger<NetCommandDispatcher> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async ValueTask<IResult?> InvokeHandler(NetPacket packet)
{
ArgumentNullException.ThrowIfNull(packet, nameof(packet));
if (_handlers.TryGetValue(packet.CmdType, out Func<IServiceProvider, NetPacket, ValueTask<IResult>>? handler))
{
return await handler(_serviceProvider, packet);
}
_logger.LogWarning("No handler defined for command of type {cmdType}", packet.CmdType);
return null;
}
private static ImmutableDictionary<CmdType, Func<IServiceProvider, NetPacket, ValueTask<IResult>>> InitializeHandlers()
{
ImmutableDictionary<CmdType, Func<IServiceProvider, NetPacket, ValueTask<IResult>>>.Builder builder
= ImmutableDictionary.CreateBuilder<CmdType, Func<IServiceProvider, NetPacket, ValueTask<IResult>>>();
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => type.GetCustomAttribute<NetControllerAttribute>() != null);
foreach (Type type in types)
{
IEnumerable<MethodInfo> methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.Where(method => method.GetCustomAttribute<NetCommandAttribute>() != null);
foreach (MethodInfo method in methods)
{
NetCommandAttribute attribute = method.GetCustomAttribute<NetCommandAttribute>()!;
if (builder.TryGetKey(attribute.CmdType, out _))
throw new Exception($"Handler for command {attribute.CmdType} defined twice!");
builder[attribute.CmdType] = CreateHandlerDelegate(type, method);
}
}
return builder.ToImmutable();
}
private static Func<IServiceProvider, NetPacket, ValueTask<IResult>> CreateHandlerDelegate(Type controllerType, MethodInfo method)
{
ParameterExpression serviceProviderParameter = Expression.Parameter(typeof(IServiceProvider), "serviceProvider");
ParameterExpression netPacketParameter = Expression.Parameter(typeof(NetPacket), "packet");
ConstantExpression controllerTypeConstant = Expression.Constant(controllerType);
ParameterExpression controllerVariable = Expression.Variable(controllerType, "controller");
PropertyInfo packetProperty = typeof(ControllerBase).GetProperty("Packet")!;
MethodInfo createInstanceMethod = typeof(ActivatorUtilities).GetMethod("CreateInstance", new Type[] { typeof(IServiceProvider), typeof(Type), typeof(object[]) })!;
List<Expression> expressionBlock = new()
{
Expression.Assign(controllerVariable, Expression.Convert(
Expression.Call(null, createInstanceMethod, serviceProviderParameter, controllerTypeConstant, Expression.Constant(Array.Empty<object>())),
controllerType)),
Expression.Assign(Expression.Property(controllerVariable, packetProperty), netPacketParameter)
};
List<Expression> parameterExpressions = new();
foreach (ParameterInfo parameter in method.GetParameters())
{
MethodInfo getServiceMethod = typeof(ServiceProviderServiceExtensions)
.GetMethod("GetRequiredService", new Type[] { typeof(IServiceProvider) })!
.MakeGenericMethod(parameter.ParameterType);
parameterExpressions.Add(Expression.Call(getServiceMethod, serviceProviderParameter));
}
expressionBlock.Add(Expression.Call(controllerVariable, method, parameterExpressions));
return Expression.Lambda<Func<IServiceProvider, NetPacket, ValueTask<IResult>>>(
Expression.Block(new[] { controllerVariable }, expressionBlock),
serviceProviderParameter,
netPacketParameter)
.Compile();
}
}

View file

@ -0,0 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using FurinaImpact.Gameserver.Network;
namespace FurinaImpact.Gameserver.Controllers.Result;
internal interface IResult
{
bool NextPacket([MaybeNullWhen(false)] out NetPacket packet);
}

View file

@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using FurinaImpact.Gameserver.Network;
namespace FurinaImpact.Gameserver.Controllers.Result;
internal class SinglePacketResult : IResult
{
private NetPacket? _packet;
public SinglePacketResult(NetPacket? packet)
{
_packet = packet;
}
public bool NextPacket([MaybeNullWhen(false)] out NetPacket packet)
{
packet = _packet;
_packet = null;
return packet != null;
}
}

View file

@ -0,0 +1,57 @@
using FurinaImpact.Gameserver.Controllers.Attributes;
using FurinaImpact.Gameserver.Controllers.Result;
using FurinaImpact.Gameserver.Game.Scene;
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Controllers;
[NetController]
internal class SceneController : ControllerBase
{
// TODO: Scene management, Entity management!!!
public const uint WeaponEntityId = 100663300;
[NetCommand(CmdType.PostEnterSceneReq)]
public async ValueTask<IResult> OnPostEnterSceneReq(SceneManager sceneManager)
{
await sceneManager.OnEnterStateChanged(SceneEnterState.PostEnter);
return Response(CmdType.PostEnterSceneRsp, new PostEnterSceneRsp
{
EnterSceneToken = sceneManager.EnterToken
});
}
[NetCommand(CmdType.EnterSceneDoneReq)]
public async ValueTask<IResult> OnEnterSceneDoneReq(SceneManager sceneManager)
{
await sceneManager.OnEnterStateChanged(SceneEnterState.EnterDone);
return Response(CmdType.EnterSceneDoneRsp, new EnterSceneDoneRsp
{
EnterSceneToken = sceneManager.EnterToken
});
}
[NetCommand(CmdType.SceneInitFinishReq)]
public async ValueTask<IResult> OnSceneInitFinishReq(SceneManager sceneManager)
{
await sceneManager.OnEnterStateChanged(SceneEnterState.InitFinished);
return Response(CmdType.SceneInitFinishRsp, new SceneInitFinishRsp
{
EnterSceneToken = sceneManager.EnterToken
});
}
[NetCommand(CmdType.EnterSceneReadyReq)]
public async ValueTask<IResult> OnEnterSceneReadyReq(SceneManager sceneManager)
{
await sceneManager.OnEnterStateChanged(SceneEnterState.ReadyToEnter);
return Response(CmdType.EnterSceneReadyRsp, new EnterSceneReadyRsp
{
EnterSceneToken = sceneManager.EnterToken
});
}
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FurinaImpact.Common\FurinaImpact.Common.csproj" />
<ProjectReference Include="..\FurinaImpact.Kcp\FurinaImpact.Kcp.csproj" />
<ProjectReference Include="..\FurinaImpact.Protocol\FurinaImpact.Protocol.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,135 @@
using FurinaImpact.Common.Constants;
using FurinaImpact.Common.Data.Excel;
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Game.Avatar;
internal class GameAvatar
{
public const ulong WeaponGuid = 2281337;
public ulong Guid { get; set; }
public uint AvatarId { get; set; }
public uint SkillDepotId { get; set; }
public uint WearingFlycloakId { get; set; }
public uint BornTime { get; set; }
public uint WeaponId { get; set; } // TODO: Weapon class!
// Properties
public List<PropValue> Properties;
public List<FightPropPair> FightProperties;
public GameAvatar()
{
Properties = new List<PropValue>();
FightProperties = new List<FightPropPair>();
}
public void InitDefaultProps(AvatarExcel avatarExcel)
{
Properties.Clear();
FightProperties.Clear();
SetProp(PlayerProp.PROP_LEVEL, 1);
SetProp(PlayerProp.PROP_EXP, 0);
SetProp(PlayerProp.PROP_BREAK_LEVEL, 0);
float baseHp = (float)avatarExcel.HpBase;
float baseAttack = (float)avatarExcel.AttackBase;
float baseDefense = (float)avatarExcel.DefenseBase;
SetFightProp(FightProp.FIGHT_PROP_BASE_HP, baseHp);
SetFightProp(FightProp.FIGHT_PROP_CUR_HP, baseHp);
SetFightProp(FightProp.FIGHT_PROP_MAX_HP, baseHp);
SetFightProp(FightProp.FIGHT_PROP_BASE_ATTACK, baseAttack);
SetFightProp(FightProp.FIGHT_PROP_CUR_ATTACK, baseAttack);
SetFightProp(FightProp.FIGHT_PROP_BASE_DEFENSE, baseDefense);
SetFightProp(FightProp.FIGHT_PROP_CUR_DEFENSE, baseDefense);
SetFightProp(FightProp.FIGHT_PROP_CHARGE_EFFICIENCY, (float)avatarExcel.ChargeEfficiency);
SetFightProp(FightProp.FIGHT_PROP_CRITICAL_HURT, (float)avatarExcel.CriticalHurt);
SetFightProp(FightProp.FIGHT_PROP_CRITICAL, (float)avatarExcel.Critical);
SetFightProp(FightProp.FIGHT_PROP_CUR_FIRE_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_CUR_ELEC_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_CUR_WATER_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_CUR_GRASS_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_CUR_WIND_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_CUR_ICE_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_CUR_ROCK_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_MAX_FIRE_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_MAX_ELEC_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_MAX_WATER_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_MAX_GRASS_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_MAX_WIND_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_MAX_ICE_ENERGY, 100);
SetFightProp(FightProp.FIGHT_PROP_MAX_ROCK_ENERGY, 100);
}
public void SetProp(uint propType, uint value)
{
PropValue? prop = Properties.Find(p => p.Type == propType);
if (prop != null)
{
prop.Ival = value;
return;
}
Properties.Add(new PropValue { Type = propType, Ival = value });
}
public void SetProp(uint propType, float value)
{
PropValue? prop = Properties.Find(p => p.Type == propType);
if (prop != null)
{
prop.Fval = value;
return;
}
Properties.Add(new PropValue { Type = propType, Fval = value });
}
public void SetFightProp(uint propType, float value)
{
FightPropPair? fightPropPair = FightProperties.Find(pair => pair.PropType == propType);
if (fightPropPair != null)
{
fightPropPair.PropValue = value;
return;
}
FightProperties.Add(new FightPropPair { PropType = propType, PropValue = value });
}
public AvatarInfo AsAvatarInfo()
{
AvatarInfo info = new()
{
Guid = Guid,
AvatarId = AvatarId,
SkillDepotId = SkillDepotId,
LifeState = 1,
AvatarType = 1,
WearingFlycloakId = WearingFlycloakId,
BornTime = BornTime,
FetterInfo = new() { ExpLevel = 1 },
EquipGuidList = { WeaponGuid } // no weapon classes for now
};
foreach (PropValue prop in Properties)
{
info.PropMap.Add(prop.Type, prop);
}
foreach (FightPropPair pair in FightProperties)
{
info.FightPropMap.Add(pair.PropType, pair.PropValue);
}
return info;
}
}

View file

@ -0,0 +1,11 @@
namespace FurinaImpact.Gameserver.Game.Avatar;
internal class GameAvatarTeam
{
public uint Index { get; set; }
public List<ulong> AvatarGuidList { get; set; }
public GameAvatarTeam()
{
AvatarGuidList = new();
}
}

View file

@ -0,0 +1,99 @@
using FurinaImpact.Common.Constants;
using FurinaImpact.Common.Data.Binout;
using FurinaImpact.Common.Data.Binout.Ability;
using FurinaImpact.Common.Extensions;
using FurinaImpact.Gameserver.Controllers;
using FurinaImpact.Gameserver.Game.Avatar;
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Game.Entity;
internal class AvatarEntity : SceneEntity
{
public override ProtEntityType EntityType => ProtEntityType.Avatar;
public uint Uid { get; }
public GameAvatar GameAvatar { get; }
public AvatarEntity(GameAvatar gameAvatar, uint uid, uint entityId) : base(entityId)
{
Uid = uid;
GameAvatar = gameAvatar;
Properties = gameAvatar.Properties;
FightProperties = gameAvatar.FightProperties;
}
public AbilityControlBlock BuildAbilityControlBlock(BinDataCollection binData)
{
AbilityControlBlock abilityControlBlock = new();
AvatarConfig avatarConfig = binData.GetAvatarConfig(GameAvatar.AvatarId);
uint defaultOverrideHash = "Default".GetStableHash();
foreach (string abilityName in AvatarConstants.CommonAbilities)
{
abilityControlBlock.AbilityEmbryoList.Add(new AbilityEmbryo
{
AbilityId = (uint)(abilityControlBlock.AbilityEmbryoList.Count + 1),
AbilityNameHash = abilityName.GetStableHash(),
AbilityOverrideNameHash = defaultOverrideHash
});
}
foreach (AbilityData ability in avatarConfig.Abilities)
{
abilityControlBlock.AbilityEmbryoList.Add(new AbilityEmbryo
{
AbilityId = (uint)(abilityControlBlock.AbilityEmbryoList.Count + 1),
AbilityNameHash = ability.AbilityName.GetStableHash(),
AbilityOverrideNameHash = ability.GetAbilityOverride().GetStableHash()
});
}
return abilityControlBlock;
}
public override SceneEntityInfo AsInfo()
{
SceneEntityInfo info = base.AsInfo();
info.Avatar = new()
{
Uid = Uid,
AvatarId = GameAvatar.AvatarId,
Guid = GameAvatar.Guid,
PeerId = 1,
EquipIdList = { GameAvatar.WeaponId },
SkillDepotId = GameAvatar.SkillDepotId,
Weapon = new SceneWeaponInfo
{
EntityId = SceneController.WeaponEntityId,
GadgetId = 50000000 + GameAvatar.WeaponId,
ItemId = GameAvatar.WeaponId,
Guid = GameAvatar.WeaponGuid,
Level = 1,
PromoteLevel = 0,
AbilityInfo = new()
},
CoreProudSkillLevel = 0,
InherentProudSkillList = { 832301 },
SkillLevelMap =
{
{ 10832, 1 },
{ 10835, 1 },
{ 10831, 1 }
},
ProudSkillExtraLevelMap =
{
{ 8331, 0 },
{ 8332, 0 },
{ 8339, 0 }
},
TeamResonanceList = { 10301 },
WearingFlycloakId = GameAvatar.WearingFlycloakId,
BornTime = GameAvatar.BornTime,
CostumeId = 0,
AnimHash = 0
};
return info;
}
}

View file

@ -0,0 +1,26 @@
using FurinaImpact.Gameserver.Game.Entity.Listener;
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Game.Entity;
internal class EntityManager
{
private readonly List<SceneEntity> _entities;
private readonly IEntityEventListener _listener;
public EntityManager(IEntityEventListener listener)
{
_entities = new List<SceneEntity>();
_listener = listener;
}
public async ValueTask SpawnEntityAsync(SceneEntity entity, VisionType visionType)
{
_entities.Add(entity);
await _listener.OnEntitySpawned(entity, visionType);
}
public void Reset()
{
_entities.Clear();
}
}

View file

@ -0,0 +1,12 @@
using FurinaImpact.Gameserver.Game.Avatar;
namespace FurinaImpact.Gameserver.Game.Entity.Factory;
internal class EntityFactory
{
private uint _entityIdSeed;
public AvatarEntity CreateAvatar(GameAvatar gameAvatar, uint belongUid)
{
return new(gameAvatar, belongUid, ++_entityIdSeed);
}
}

View file

@ -0,0 +1,7 @@
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Game.Entity.Listener;
internal interface IEntityEventListener
{
ValueTask OnEntitySpawned(SceneEntity entity, VisionType visionType);
}

View file

@ -0,0 +1,69 @@
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Game.Entity;
internal abstract class SceneEntity
{
public abstract ProtEntityType EntityType { get; }
public uint EntityId { get; }
public MotionInfo MotionInfo { get; set; }
public List<PropValue> Properties { get; set; }
public List<FightPropPair> FightProperties { get; set; }
public SceneEntity(uint entityId)
{
EntityId = ((uint)EntityType << 24) + entityId;
MotionInfo = new() { Pos = new(), Rot = new(), Speed = new() };
Properties = new();
FightProperties = new();
}
public void SetPosition(float x, float y, float z)
{
MotionInfo.Pos.X = x;
MotionInfo.Pos.Y = y;
MotionInfo.Pos.Z = z;
}
public void SetRotation(float x, float y, float z)
{
MotionInfo.Rot.X = x;
MotionInfo.Rot.Y = y;
MotionInfo.Rot.Z = z;
}
public virtual SceneEntityInfo AsInfo()
{
SceneEntityInfo info = new()
{
EntityType = EntityType,
EntityId = EntityId,
MotionInfo = MotionInfo,
LifeState = 1,
EntityClientData = new(),
EntityAuthorityInfo = new EntityAuthorityInfo
{
AbilityInfo = new(),
AiInfo = new()
{
IsAiOpen = true,
BornPos = new()
},
BornPos = new(),
ClientExtraInfo = new(),
RendererChangedInfo = new()
},
AnimatorParaList = { new AnimatorParameterValueInfoPair() }
};
foreach (PropValue prop in Properties)
{
info.PropList.Add(new PropPair { Type = prop.Type, PropValue = prop });
}
info.FightPropList.AddRange(FightProperties);
return info;
}
}

View file

@ -0,0 +1,82 @@
using System.Diagnostics.CodeAnalysis;
using FurinaImpact.Common.Data.Excel;
using FurinaImpact.Gameserver.Game.Avatar;
using FurinaImpact.Gameserver.Game.Scene;
namespace FurinaImpact.Gameserver.Game;
internal class Player
{
private static readonly uint[] AvatarBlackList = { 10000001, 10000005, 10000007 }; // kate and travelers
public uint Uid { get; set; }
public uint GuidSeed { get; set; }
public string Name { get; set; }
public List<GameAvatar> Avatars { get; set; }
public List<GameAvatarTeam> AvatarTeams { get; set; }
public uint CurTeamIndex { get; set; }
private readonly ExcelTableCollection _excelTables;
public Player(ExcelTableCollection excelTables)
{
Name = "Traveler";
Avatars = new();
AvatarTeams = new();
_excelTables = excelTables;
}
public void InitDefaultPlayer()
{
// We don't have database atm, so let's init default player state for every session.
Uid = 1337;
Name = "FurinaImpact";
UnlockAllAvatars();
_ = TryGetAvatar(10000089, out GameAvatar? avatar);
AvatarTeams.Add(new()
{
AvatarGuidList = new() { avatar!.Guid },
Index = 1
});
CurTeamIndex = 1;
}
public GameAvatarTeam GetCurrentTeam()
=> AvatarTeams.Find(team => team.Index == CurTeamIndex)!;
public bool TryGetAvatar(uint avatarId, [MaybeNullWhen(false)] out GameAvatar avatar)
=> (avatar = Avatars.Find(a => a.AvatarId == avatarId)) != null;
private void UnlockAllAvatars()
{
ExcelTable avatarTable = _excelTables.GetTable(ExcelType.Avatar);
for (int i = 0; i < avatarTable.Count; i++)
{
AvatarExcel avatarExcel = avatarTable.GetItemAt<AvatarExcel>(i);
if (AvatarBlackList.Contains(avatarExcel.Id) || avatarExcel.Id >= 11000000) continue;
uint currentTimestamp = (uint)DateTimeOffset.Now.ToUnixTimeSeconds();
GameAvatar avatar = new()
{
AvatarId = avatarExcel.Id,
SkillDepotId = avatarExcel.SkillDepotId,
WeaponId = avatarExcel.InitialWeapon,
BornTime = currentTimestamp,
Guid = NextGuid(),
WearingFlycloakId = 140001
};
avatar.InitDefaultProps(avatarExcel);
Avatars.Add(avatar);
}
}
public ulong NextGuid()
{
return ((ulong)Uid << 32) + (++GuidSeed);
}
}

View file

@ -0,0 +1,12 @@
namespace FurinaImpact.Gameserver.Game.Scene;
internal enum SceneEnterState
{
None = -1,
EnterRequested,
ReadyToEnter,
InitFinished,
EnterDone,
PostEnter,
Complete
}

View file

@ -0,0 +1,224 @@
using System.Numerics;
using FurinaImpact.Common.Data.Binout;
using FurinaImpact.Gameserver.Controllers;
using FurinaImpact.Gameserver.Game.Avatar;
using FurinaImpact.Gameserver.Game.Entity;
using FurinaImpact.Gameserver.Game.Entity.Factory;
using FurinaImpact.Gameserver.Network.Session;
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Game.Scene;
internal class SceneManager
{
public uint EnterToken { get; private set; }
private readonly BinDataCollection _binData;
private readonly NetSession _session;
private readonly Player _player;
private readonly EntityManager _entityManager;
private readonly EntityFactory _entityFactory;
private readonly List<AvatarEntity> _teamAvatars;
private uint _enterTokenSeed;
private uint _sceneId;
private ulong _beginTime;
private SceneEnterState _enterState;
public SceneManager(NetSession session, Player player, EntityManager entityManager, EntityFactory entityFactory, BinDataCollection binData)
{
_session = session;
_player = player;
_entityManager = entityManager;
_entityFactory = entityFactory;
_binData = binData;
_teamAvatars = new();
}
public async ValueTask OnEnterStateChanged(SceneEnterState changedToState)
{
if (_enterState is SceneEnterState.None or SceneEnterState.Complete)
throw new InvalidOperationException($"SceneManager::OnEnterStateChanged called when enter state is {_enterState}!");
if (_enterState > changedToState)
throw new ArgumentException($"SceneManager::OnEnterStateChanged - requested state is less than current! (curr={_enterState}, req={changedToState})");
if (_enterState + 1 != changedToState)
throw new ArgumentException($"SceneManager::OnEnterStateChanged - trying to skip enter state! (curr={_enterState}, req={changedToState})");
_enterState = changedToState;
switch (_enterState)
{
case SceneEnterState.ReadyToEnter:
await OnReadyToEnterScene();
break;
case SceneEnterState.InitFinished:
await OnSceneInitFinished();
break;
case SceneEnterState.EnterDone:
await OnEnterDone();
break;
case SceneEnterState.PostEnter:
await OnPostEnter();
break;
}
if (_enterState == SceneEnterState.PostEnter)
_enterState = SceneEnterState.Complete;
}
public async ValueTask ChangeTeamAvatarsAsync(ulong[] guidList)
{
_teamAvatars.Clear();
foreach (ulong guid in guidList)
{
GameAvatar gameAvatar = _player.Avatars.Find(avatar => avatar.Guid == guid)!; // currently only first one
AvatarEntity avatarEntity = _entityFactory.CreateAvatar(gameAvatar, _player.Uid);
avatarEntity.SetPosition(2336.789f, 249.98896f, -751.3081f);
_teamAvatars.Add(avatarEntity);
}
await SendSceneTeamUpdate();
await _entityManager.SpawnEntityAsync(_teamAvatars[0], VisionType.Born);
}
private async ValueTask OnEnterDone()
{
await _entityManager.SpawnEntityAsync(_teamAvatars[0], VisionType.Born);
}
private async ValueTask OnSceneInitFinished()
{
GameAvatarTeam avatarTeam = _player.GetCurrentTeam();
foreach (ulong guid in avatarTeam.AvatarGuidList)
{
GameAvatar gameAvatar = _player.Avatars.Find(avatar => avatar.Guid == guid)!;
AvatarEntity avatarEntity = _entityFactory.CreateAvatar(gameAvatar, _player.Uid);
avatarEntity.SetPosition(2336.789f, 249.98896f, -751.3081f);
_teamAvatars.Add(avatarEntity);
}
await SendEnterSceneInfo();
await SendSceneTeamUpdate();
}
private async ValueTask OnReadyToEnterScene()
{
await _session.NotifyAsync(CmdType.EnterScenePeerNotify, new EnterScenePeerNotify
{
DestSceneId = _sceneId,
EnterSceneToken = EnterToken,
HostPeerId = 1, // TODO: Scene peers
PeerId = 1
});
}
private ValueTask OnPostEnter()
{
return ValueTask.CompletedTask;
}
public async ValueTask EnterSceneAsync(uint sceneId)
{
if (_beginTime != 0) ResetState();
_beginTime = (ulong)DateTimeOffset.Now.ToUnixTimeSeconds();
_sceneId = sceneId;
EnterToken = ++_enterTokenSeed;
_enterState = SceneEnterState.EnterRequested;
await _session.NotifyAsync(CmdType.PlayerEnterSceneNotify, new PlayerEnterSceneNotify
{
SceneBeginTime = _beginTime,
SceneId = _sceneId,
SceneTransaction = CreateTransaction(_sceneId, _player.Uid, _beginTime),
Pos = new()
{
X = 2191.16357421875f,
Y = 214.65115356445312f,
Z = -1120.633056640625f
},
TargetUid = _player.Uid,
UnkUid1020 = _player.Uid,
EnterSceneToken = EnterToken,
PrevPos = new(),
Unk13 = 1,
Unk3 = 1,
Unk449 = 1,
Unk834 = 1
});
}
private async ValueTask SendSceneTeamUpdate()
{
SceneTeamUpdateNotify sceneTeamUpdate = new();
foreach (AvatarEntity avatar in _teamAvatars)
{
sceneTeamUpdate.SceneTeamAvatarList.Add(new SceneTeamAvatar
{
SceneEntityInfo = avatar.AsInfo(),
WeaponEntityId = SceneController.WeaponEntityId,
PlayerUid = _player.Uid,
WeaponGuid = GameAvatar.WeaponGuid,
EntityId = avatar.EntityId,
AvatarGuid = avatar.GameAvatar.Guid,
AbilityControlBlock = avatar.BuildAbilityControlBlock(_binData),
SceneId = _sceneId
});
}
await _session.NotifyAsync(CmdType.SceneTeamUpdateNotify, sceneTeamUpdate);
}
private async ValueTask SendEnterSceneInfo()
{
PlayerEnterSceneInfoNotify enterSceneInfo = new()
{
CurAvatarEntityId = _teamAvatars[0].EntityId,
EnterSceneToken = EnterToken,
MpLevelEntityInfo = new MPLevelEntityInfo
{
EntityId = 184549377,
AbilityInfo = new AbilitySyncStateInfo(),
AuthorityPeerId = 1
},
TeamEnterInfo = new TeamEnterSceneInfo
{
TeamEntityId = 150994946,
AbilityControlBlock = new AbilityControlBlock(),
TeamAbilityInfo = new AbilitySyncStateInfo()
}
};
foreach (AvatarEntity avatar in _teamAvatars)
{
enterSceneInfo.AvatarEnterInfo.Add(new AvatarEnterSceneInfo
{
AvatarGuid = avatar.GameAvatar.Guid,
AvatarEntityId = avatar.EntityId,
WeaponEntityId = SceneController.WeaponEntityId,
WeaponGuid = GameAvatar.WeaponGuid
});
}
await _session.NotifyAsync(CmdType.PlayerEnterSceneInfoNotify, enterSceneInfo);
}
private void ResetState()
{
_teamAvatars.Clear();
_entityManager.Reset();
}
private static string CreateTransaction(uint sceneId, uint playerUid, ulong beginTime)
=> string.Format("{0}-{1}-{2}-13830", sceneId, playerUid, beginTime);
}

View file

@ -0,0 +1,28 @@
using FurinaImpact.Common.Data.Binout;
using FurinaImpact.Common.Data.Excel;
using FurinaImpact.Gameserver.Network;
using Microsoft.Extensions.Hosting;
namespace FurinaImpact.Gameserver;
internal class GameServer : IHostedService
{
private readonly IGateway _gateway;
public GameServer(IGateway gateway, ExcelTableCollection excelTables, BinDataCollection binDataCollection)
{
_ = excelTables;
_ = binDataCollection;
_gateway = gateway;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await _gateway.Start();
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await _gateway.Stop();
}
}

View file

@ -0,0 +1,6 @@
namespace FurinaImpact.Gameserver.Network;
internal interface IGateway
{
Task Start();
Task Stop();
}

View file

@ -0,0 +1,10 @@
using System.Net;
namespace FurinaImpact.Gameserver.Network;
internal interface INetworkUnit : IDisposable
{
IPEndPoint RemoteEndPoint { get; }
ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken);
ValueTask SendAsync(Memory<byte> buffer, CancellationToken cancellationToken);
}

View file

@ -0,0 +1,94 @@
using System.Buffers;
using System.Net;
using System.Net.Sockets;
using FurinaImpact.Gameserver.Options;
using FurinaImpact.Kcp;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace FurinaImpact.Gameserver.Network.Kcp;
internal sealed class KcpGateway : IGateway
{
private readonly Random _random;
private readonly ILogger _logger;
private readonly IOptions<GatewayOptions> _options;
private readonly NetSessionManager _sessionManager;
private uint _sessionCounter;
private IKcpTransport<IKcpMultiplexConnection>? _transport;
public KcpGateway(ILogger<KcpGateway> logger, IOptions<GatewayOptions> options, NetSessionManager sessionManager)
{
_logger = logger;
_options = options;
_sessionManager = sessionManager;
_random = new();
}
public Task Start()
{
IPEndPoint bindEndPoint = _options.Value.EndPoint;
_transport = KcpSocketTransport.CreateMultiplexConnection(new(bindEndPoint), 1400);
_transport.SetCallbacks(20, HandleKcpHandshake);
_transport.Start();
_logger.LogInformation("KCP Gateway is up at {endPoint}", bindEndPoint);
return Task.CompletedTask;
}
private async ValueTask HandleKcpHandshake(UdpReceiveResult receiveResult)
{
KcpHandshake handshake = KcpHandshake.ReadFrom(receiveResult.Buffer);
switch ((handshake.Head, handshake.Tail))
{
case (KcpHandshake.StartConversationHead, KcpHandshake.StartConversationTail):
await OnStartConversationRequest(receiveResult.RemoteEndPoint);
break;
}
}
private async ValueTask OnStartConversationRequest(IPEndPoint clientEndPoint)
{
uint convId = Interlocked.Increment(ref _sessionCounter);
uint token = (uint)_random.Next();
long convId64 = (long)convId << 32 | token;
KcpConversation conversation = _transport!.Connection.CreateConversation(convId64, clientEndPoint);
_ = _sessionManager.RunSessionAsync(convId64, new KcpNetworkUnit(conversation, clientEndPoint));
await SendConversationCreatedPacket(clientEndPoint, convId, token);
}
private async ValueTask SendConversationCreatedPacket(IPEndPoint clientEndPoint, uint convId, uint token)
{
KcpHandshake handshakeResponse = new()
{
Head = KcpHandshake.ConversationCreatedHead,
Param1 = convId,
Param2 = token,
Data = 1234567890,
Tail = KcpHandshake.ConversationCreatedTail
};
byte[] buffer = ArrayPool<byte>.Shared.Rent(20);
try
{
Memory<byte> bufferMemory = buffer.AsMemory();
handshakeResponse.WriteTo(buffer);
await _transport!.SendPacketAsync(bufferMemory[..20], clientEndPoint, CancellationToken.None);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public Task Stop()
{
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,39 @@
using System.Buffers.Binary;
namespace FurinaImpact.Gameserver.Network.Kcp;
internal struct KcpHandshake
{
public const uint StartConversationHead = 0xFF;
public const uint StartConversationTail = 0xFFFFFFFF;
public const uint ConversationCreatedHead = 0x00000145;
public const uint ConversationCreatedTail = 0x14514545;
public const uint ConversationEndHead = 0x194;
public const uint ConversationEndTail = 0x19419494;
public uint Head { get; set; }
public uint Param1 { get; set; }
public uint Param2 { get; set; }
public uint Data { get; set; }
public uint Tail { get; set; }
public readonly void WriteTo(Span<byte> buffer)
{
BinaryPrimitives.WriteUInt32BigEndian(buffer[0..4], Head);
BinaryPrimitives.WriteUInt32LittleEndian(buffer[4..8], Param1);
BinaryPrimitives.WriteUInt32LittleEndian(buffer[8..12], Param2);
BinaryPrimitives.WriteUInt32BigEndian(buffer[12..16], Data);
BinaryPrimitives.WriteUInt32BigEndian(buffer[16..20], Tail);
}
public static KcpHandshake ReadFrom(ReadOnlySpan<byte> buffer) => new()
{
Head = BinaryPrimitives.ReadUInt32BigEndian(buffer[0..4]),
Param1 = BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]),
Param2 = BinaryPrimitives.ReadUInt32LittleEndian(buffer[8..12]),
Data = BinaryPrimitives.ReadUInt32BigEndian(buffer[12..16]),
Tail = BinaryPrimitives.ReadUInt32BigEndian(buffer[16..20])
};
}

View file

@ -0,0 +1,35 @@
using System.Net;
using FurinaImpact.Kcp;
namespace FurinaImpact.Gameserver.Network.Kcp;
internal class KcpNetworkUnit : INetworkUnit
{
public IPEndPoint RemoteEndPoint { get; }
private readonly KcpConversation _conversation;
public KcpNetworkUnit(KcpConversation conversation, IPEndPoint remoteEndPoint)
{
_conversation = conversation;
RemoteEndPoint = remoteEndPoint;
}
public async ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken)
{
KcpConversationReceiveResult result = await _conversation.ReceiveAsync(buffer, cancellationToken);
if (result.TransportClosed)
return -1;
return result.BytesReceived;
}
public async ValueTask SendAsync(Memory<byte> buffer, CancellationToken cancellationToken)
{
await _conversation.SendAsync(buffer, cancellationToken);
}
public void Dispose()
{
_conversation.Dispose();
}
}

View file

@ -0,0 +1,48 @@
using FurinaImpact.Common.Security;
using FurinaImpact.Gameserver.Controllers.Dispatching;
using FurinaImpact.Gameserver.Network.Session;
using Microsoft.Extensions.Logging;
namespace FurinaImpact.Gameserver.Network.Kcp;
internal class KcpSession : NetSession
{
private const int MaxPacketSize = 32768;
private const int ReadTimeout = 30;
private const int WriteTimeout = 30;
private readonly byte[] _recvBuffer;
private readonly byte[] _sendBuffer;
public KcpSession(ILogger<NetSession> logger, NetSessionManager sessionManager, NetCommandDispatcher commandDispatcher) : base(logger, sessionManager, commandDispatcher)
{
_recvBuffer = GC.AllocateUninitializedArray<byte>(MaxPacketSize);
_sendBuffer = GC.AllocateUninitializedArray<byte>(MaxPacketSize);
}
public override async ValueTask RunAsync()
{
Memory<byte> buffer = _recvBuffer.AsMemory();
while (true)
{
int readAmount = await ReadWithTimeoutAsync(buffer, ReadTimeout);
if (readAmount <= 0)
break;
MhySecurity.Xor(buffer[..readAmount].Span, EncryptionKey);
int consumedBytes = await ConsumePacketsAsync(buffer[..readAmount]);
if (consumedBytes == -1)
break;
}
}
public override async ValueTask SendAsync(NetPacket packet)
{
Memory<byte> buffer = _sendBuffer.AsMemory();
int length = packet.EncodeTo(buffer);
MhySecurity.Xor(buffer[..length].Span, EncryptionKey);
await WriteWithTimeoutAsync(buffer[..length], WriteTimeout);
}
}

View file

@ -0,0 +1,67 @@
using System.Buffers.Binary;
using FurinaImpact.Protocol;
using Google.Protobuf;
namespace FurinaImpact.Gameserver.Network;
internal class NetPacket
{
private const ushort HeadMagic = 0x4567;
private const ushort TailMagic = 0x89AB;
public CmdType CmdType { get; set; }
public Memory<byte> Head { get; set; }
public Memory<byte> Body { get; set; }
public TBody DecodeBody<TBody>() where TBody : IMessage<TBody>, new()
{
return new MessageParser<TBody>(() => new()).ParseFrom(Body.Span);
}
public int EncodeTo(Memory<byte> buffer)
{
Span<byte> span = buffer.Span;
BinaryPrimitives.WriteUInt16BigEndian(span[0..2], HeadMagic);
BinaryPrimitives.WriteUInt16BigEndian(span[2..4], (ushort)CmdType);
BinaryPrimitives.WriteUInt16BigEndian(span[4..6], (ushort)Head.Length);
BinaryPrimitives.WriteInt32BigEndian(span[6..10], Body.Length);
Head.CopyTo(buffer[10..]);
Body.CopyTo(buffer[(10 + Head.Length)..]);
BinaryPrimitives.WriteUInt16BigEndian(span[(10 + Head.Length + Body.Length)..], TailMagic);
return 12 + Head.Length + Body.Length;
}
public static (NetPacket?, int) DecodeFrom(Memory<byte> data)
{
ReadOnlySpan<byte> span = data.Span;
ushort headMagic = BinaryPrimitives.ReadUInt16BigEndian(span[0..2]);
if (headMagic != HeadMagic)
return (null, 0);
ushort cmdType = BinaryPrimitives.ReadUInt16BigEndian(span[2..4]);
int headLength = BinaryPrimitives.ReadUInt16BigEndian(span[4..6]);
int bodyLength = BinaryPrimitives.ReadInt32BigEndian(span[6..10]);
if (data.Length < 12 + headLength + bodyLength)
return (null, 0);
Memory<byte> head = data.Slice(10, headLength);
Memory<byte> body = data.Slice(10 + headLength, bodyLength);
ushort tailMagic = BinaryPrimitives.ReadUInt16BigEndian(span[(10 + headLength + bodyLength)..]);
if (tailMagic != TailMagic)
return (null, 0);
NetPacket netPacket = new()
{
CmdType = (CmdType)cmdType,
Head = head,
Body = body
};
return (netPacket, 12 + headLength + bodyLength);
}
}

View file

@ -0,0 +1,57 @@
using System.Collections.Concurrent;
using FurinaImpact.Gameserver.Network.Session;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace FurinaImpact.Gameserver.Network;
internal class NetSessionManager
{
private readonly ConcurrentDictionary<long, NetSession> _sessions;
private readonly ILogger _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
public NetSessionManager(ILogger<NetSessionManager> logger, IServiceScopeFactory serviceScopeFactory)
{
_sessions = new();
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
public async Task RunSessionAsync(long sessionId, INetworkUnit networkUnit)
{
await using AsyncServiceScope serviceScope = _serviceScopeFactory.CreateAsyncScope();
NetSession session = serviceScope.ServiceProvider.GetRequiredService<NetSession>();
try
{
session.Establish(sessionId, networkUnit);
await session.RunAsync();
}
catch (OperationCanceledException)
{
// OperationCanceled
}
catch (Exception exception)
{
_logger.LogError("Exception occurred during handling a session, trace: {exception}", exception);
}
}
public void Add(NetSession session)
{
_sessions[session.SessionId] = session;
_logger.LogInformation("New connection from {endPoint}", session.EndPoint);
}
public bool TryRemove(NetSession session)
{
bool removed = _sessions.TryRemove(session.SessionId, out _);
if (removed)
{
_logger.LogInformation("Client from {endPoint} disconnected", session.EndPoint);
}
return removed;
}
}

View file

@ -0,0 +1,109 @@
using System.Net;
using FurinaImpact.Common.Security;
using FurinaImpact.Gameserver.Controllers.Dispatching;
using FurinaImpact.Gameserver.Controllers.Result;
using FurinaImpact.Protocol;
using Google.Protobuf;
using Microsoft.Extensions.Logging;
namespace FurinaImpact.Gameserver.Network.Session;
internal abstract class NetSession : IDisposable
{
public IPEndPoint EndPoint => _networkUnit!.RemoteEndPoint;
public long SessionId { get; private set; }
private INetworkUnit? _networkUnit;
private readonly ILogger _logger;
private readonly NetSessionManager _sessionManager;
private readonly NetCommandDispatcher _commandDispatcher;
protected byte[] EncryptionKey { get; private set; }
public NetSession(ILogger<NetSession> logger, NetSessionManager sessionManager, NetCommandDispatcher commandDispatcher)
{
_logger = logger;
_sessionManager = sessionManager;
_commandDispatcher = commandDispatcher;
EncryptionKey = MhySecurity.InitialKey;
}
public abstract ValueTask RunAsync();
public abstract ValueTask SendAsync(NetPacket packet);
public void Establish(long sessionId, INetworkUnit networkUnit)
{
SessionId = sessionId;
_networkUnit = networkUnit;
_sessionManager.Add(this);
}
public async Task NotifyAsync<TNotify>(CmdType cmdType, TNotify notify) where TNotify : IMessage<TNotify>
{
await SendAsync(new()
{
CmdType = cmdType,
Head = Memory<byte>.Empty,
Body = notify.ToByteArray()
});
}
protected async ValueTask<int> ConsumePacketsAsync(Memory<byte> buffer)
{
if (buffer.Length < 12)
return 0;
int consumed = 0;
do
{
(NetPacket? packet, int bytesConsumed) = NetPacket.DecodeFrom(buffer[consumed..]);
consumed += bytesConsumed;
if (packet == null)
return consumed;
IResult? result = await _commandDispatcher.InvokeHandler(packet);
if (result != null)
{
while (result.NextPacket(out NetPacket? serverPacket))
{
await SendAsync(serverPacket);
if (serverPacket.CmdType == CmdType.GetPlayerTokenRsp)
{
InitializeEncryption(1337); // hardcoded MT seed with patch
}
}
_logger.LogInformation("Successfully handled command of type {cmdType}", packet.CmdType);
}
} while (buffer.Length - consumed >= 12);
return consumed;
}
private void InitializeEncryption(ulong seed)
{
EncryptionKey = MhySecurity.GenerateSecretKey(seed);
}
protected async ValueTask<int> ReadWithTimeoutAsync(Memory<byte> buffer, int timeoutSeconds)
{
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(timeoutSeconds));
return await _networkUnit!.ReceiveAsync(buffer, cancellationTokenSource.Token);
}
protected async ValueTask WriteWithTimeoutAsync(Memory<byte> buffer, int timeoutSeconds)
{
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(timeoutSeconds));
await _networkUnit!.SendAsync(buffer, cancellationTokenSource.Token);
}
public virtual void Dispose()
{
_networkUnit?.Dispose();
_ = _sessionManager.TryRemove(this);
}
}

View file

@ -0,0 +1,23 @@
using FurinaImpact.Gameserver.Game.Entity;
using FurinaImpact.Gameserver.Game.Entity.Listener;
using FurinaImpact.Protocol;
namespace FurinaImpact.Gameserver.Network.Session;
internal class SessionEntityEventListener : IEntityEventListener
{
private readonly NetSession _session;
public SessionEntityEventListener(NetSession session)
{
_session = session;
}
public async ValueTask OnEntitySpawned(SceneEntity entity, VisionType visionType)
{
await _session.NotifyAsync(CmdType.SceneEntityAppearNotify, new SceneEntityAppearNotify
{
AppearType = visionType,
EntityList = { entity.AsInfo() }
});
}
}

View file

@ -0,0 +1,12 @@
using System.Net;
namespace FurinaImpact.Gameserver.Options;
internal record GatewayOptions
{
public const string Section = "Gateway";
public required string Host { get; set; }
public required int Port { get; set; }
public IPEndPoint EndPoint => new(IPAddress.Parse(Host), Port);
}

View file

@ -0,0 +1,50 @@
using FurinaImpact.Common.Data;
using FurinaImpact.Common.Data.Binout;
using FurinaImpact.Common.Data.Excel;
using FurinaImpact.Common.Data.Provider;
using FurinaImpact.Gameserver;
using FurinaImpact.Gameserver.Controllers.Dispatching;
using FurinaImpact.Gameserver.Game;
using FurinaImpact.Gameserver.Game.Entity;
using FurinaImpact.Gameserver.Game.Entity.Factory;
using FurinaImpact.Gameserver.Game.Entity.Listener;
using FurinaImpact.Gameserver.Game.Scene;
using FurinaImpact.Gameserver.Network;
using FurinaImpact.Gameserver.Network.Kcp;
using FurinaImpact.Gameserver.Network.Session;
using FurinaImpact.Gameserver.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
Console.Title = "FurinaImpact | Game Server [Experimental]";
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Logging.AddSimpleConsole();
builder.Services.Configure<GatewayOptions>(builder.Configuration.GetSection(GatewayOptions.Section));
// Resources
builder.Services.UseLocalAssets();
builder.Services.AddSingleton<DataHelper>();
builder.Services.AddSingleton<ExcelTableCollection>();
builder.Services.AddSingleton<BinDataCollection>();
// Game Logic
builder.Services.AddScoped<Player>();
builder.Services.AddScoped<SceneManager>();
builder.Services.AddScoped<EntityManager>();
builder.Services.AddScoped<EntityFactory>();
// Logic Listeners
builder.Services.AddScoped<IEntityEventListener, SessionEntityEventListener>();
// Network
builder.Services.AddScoped<NetCommandDispatcher>();
builder.Services.AddScoped<NetSession, KcpSession>();
builder.Services.AddSingleton<IGateway, KcpGateway>();
builder.Services.AddSingleton<NetSessionManager>();
builder.Services.AddHostedService<GameServer>();
await builder.Build().RunAsync();

View file

@ -0,0 +1,6 @@
{
"Gateway": {
"Host": "0.0.0.0",
"Port": 22101
}
}

View file

@ -0,0 +1,22 @@
#if !NEED_POH_SHIM
using System.Buffers;
namespace FurinaImpact.Kcp
{
internal sealed class ArrayMemoryOwner : IMemoryOwner<byte>
{
private readonly byte[] _buffer;
public ArrayMemoryOwner(byte[] buffer)
{
_buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
}
public Memory<byte> Memory => _buffer;
public void Dispose() { }
}
}
#endif

View file

@ -0,0 +1,117 @@
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace FurinaImpact.Kcp
{
internal class AsyncAutoResetEvent<T> : IValueTaskSource<T>
{
private ManualResetValueTaskSourceCore<T> _rvtsc;
private SpinLock _lock;
private bool _isSet;
private bool _activeWait;
private bool _signaled;
private T? _value;
public AsyncAutoResetEvent()
{
_rvtsc = new ManualResetValueTaskSourceCore<T>()
{
RunContinuationsAsynchronously = true
};
_lock = new SpinLock();
}
T IValueTaskSource<T>.GetResult(short token)
{
try
{
return _rvtsc.GetResult(token);
}
finally
{
_rvtsc.Reset();
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_activeWait = false;
_signaled = false;
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
}
}
ValueTaskSourceStatus IValueTaskSource<T>.GetStatus(short token) => _rvtsc.GetStatus(token);
void IValueTaskSource<T>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
=> _rvtsc.OnCompleted(continuation, state, token, flags);
public ValueTask<T> WaitAsync()
{
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if (_activeWait)
{
return new ValueTask<T>(Task.FromException<T>(new InvalidOperationException("Another thread is already waiting.")));
}
if (_isSet)
{
_isSet = false;
T value = _value!;
_value = default;
return new ValueTask<T>(value);
}
_activeWait = true;
Debug.Assert(!_signaled);
return new ValueTask<T>(this, _rvtsc.Version);
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
}
public void Set(T value)
{
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if (_activeWait && !_signaled)
{
_signaled = true;
_rvtsc.SetResult(value);
return;
}
_isSet = true;
_value = value;
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
}
}
}

View file

@ -0,0 +1,13 @@
namespace FurinaImpact.Kcp
{
internal sealed class DefaultArrayPoolBufferAllocator : IKcpBufferPool
{
public static DefaultArrayPoolBufferAllocator Default { get; } = new DefaultArrayPoolBufferAllocator();
public KcpRentedBuffer Rent(KcpBufferPoolRentOptions options)
{
return KcpRentedBuffer.FromSharedArrayPool(options.Size);
}
}
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,15 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// The buffer pool to rent buffers from.
/// </summary>
public interface IKcpBufferPool
{
/// <summary>
/// Rent a buffer using the specified options.
/// </summary>
/// <param name="options">The options used to rent this buffer.</param>
/// <returns></returns>
KcpRentedBuffer Rent(KcpBufferPoolRentOptions options);
}
}

View file

@ -0,0 +1,23 @@
using System.Net.Sockets;
namespace FurinaImpact.Kcp
{
/// <summary>
/// A conversation or a channel over the transport.
/// </summary>
public interface IKcpConversation : IDisposable
{
/// <summary>
/// Put message into the receive queue of the channel.
/// </summary>
/// <param name="packet">The packet content with the optional conversation ID. This buffer should not contain space for pre-buffer and post-buffer.</param>
/// <param name="cancellationToken">The token to cancel this operation.</param>
/// <returns>A <see cref="ValueTask"/> that completes when the packet is put into the receive queue.</returns>
ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken);
/// <summary>
/// Mark the underlying transport as closed. Abort all active send or receive operations.
/// </summary>
void SetTransportClosed();
}
}

View file

@ -0,0 +1,8 @@
namespace FurinaImpact.Kcp
{
internal interface IKcpConversationUpdateNotificationSource
{
ReadOnlyMemory<byte> Packet { get; }
void Release();
}
}

View file

@ -0,0 +1,16 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// An instance that can produce exceptions in background jobs.
/// </summary>
/// <typeparam name="T">The type of the instance.</typeparam>
public interface IKcpExceptionProducer<out T>
{
/// <summary>
/// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation.
/// </summary>
/// <param name="handler">The exception handler.</param>
/// <param name="state">The state object to pass into the exception handler.</param>
void SetExceptionHandler(Func<Exception, T, object, bool> handler, object state);
}
}

View file

@ -0,0 +1,57 @@
using System.Net;
namespace FurinaImpact.Kcp
{
/// <summary>
/// Multiplex many channels or conversations over the same transport.
/// </summary>
public interface IKcpMultiplexConnection : IDisposable
{
/// <summary>
/// Determine whether the multiplex connection contains a conversation with the specified id.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <returns>True if the multiplex connection contains the specified conversation. Otherwise false.</returns>
bool Contains(long id);
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote endpoint</param>
/// <param name="options">The options of the <see cref="KcpRawChannel"/>.</param>
/// <returns>The raw channel created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, KcpRawChannelOptions options = null);
/// <summary>
/// Create a conversation with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote endpoint</param>
/// <param name="options">The options of the <see cref="KcpConversation"/>.</param>
/// <returns>The KCP conversation created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, KcpConversationOptions options = null);
/// <summary>
/// Register a conversation or channel with the specified conversation ID and user state.
/// </summary>
/// <param name="conversation">The conversation or channel to register.</param>
/// <param name="id">The conversation ID.</param>
/// <exception cref="ArgumentNullException"><paramref name="conversation"/> is not provided.</exception>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
void RegisterConversation(IKcpConversation conversation, long id);
/// <summary>
/// Unregister a conversation or channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <returns>The conversation unregistered. Returns null when the conversation with the specified ID is not found.</returns>
IKcpConversation UnregisterConversation(long id);
}
}

View file

@ -0,0 +1,53 @@
using System.Net;
namespace FurinaImpact.Kcp
{
/// <summary>
/// Multiplex many channels or conversations over the same transport.
/// </summary>
public interface IKcpMultiplexConnection<T> : IKcpMultiplexConnection
{
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="state">The user state of this channel.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel"/>.</param>
/// <returns>The raw channel created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, T state, KcpRawChannelOptions options = null);
/// <summary>
/// Create a conversation with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="state">The user state of this conversation.</param>
/// <param name="options">The options of the <see cref="KcpConversation"/>.</param>
/// <returns>The KCP conversation created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, T state, KcpConversationOptions options = null);
/// <summary>
/// Register a conversation or channel with the specified conversation ID and user state.
/// </summary>
/// <param name="conversation">The conversation or channel to register.</param>
/// <param name="id">The conversation ID.</param>
/// <param name="state">The user state</param>
/// <exception cref="ArgumentNullException"><paramref name="conversation"/> is not provided.</exception>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
void RegisterConversation(IKcpConversation conversation, long id, T? state);
/// <summary>
/// Unregister a conversation or channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="state">The user state.</param>
/// <returns>The conversation unregistered with the user state. Returns default when the conversation with the specified ID is not found.</returns>
IKcpConversation UnregisterConversation(long id, out T? state);
}
}

View file

@ -0,0 +1,22 @@
using System.Net;
using System.Net.Sockets;
namespace FurinaImpact.Kcp
{
/// <summary>
/// A transport to send and receive packets.
/// </summary>
public interface IKcpTransport
{
/// <summary>
/// Send a packet into the transport.
/// </summary>
/// <param name="packet">The content of the packet.</param>
/// <param name="remoteEndpoint">The remote endpoint</param>
/// <param name="cancellationToken">A token to cancel this operation.</param>
/// <returns>A <see cref="ValueTask"/> that completes when the packet is sent.</returns>
ValueTask SendPacketAsync(Memory<byte> packet, IPEndPoint remoteEndpoint, CancellationToken cancellationToken);
void SetCallbacks(int handshakeSize, Func<UdpReceiveResult, ValueTask> handshakeHandler);
}
}

View file

@ -0,0 +1,22 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// A transport instance for upper-level connections.
/// </summary>
/// <typeparam name="T">The type of the upper-level connection.</typeparam>
public interface IKcpTransport<out T> : IKcpTransport, IKcpExceptionProducer<IKcpTransport<T>>, IDisposable
{
/// <summary>
/// Get the upper-level connection instace. If Start is not called or the transport is closed, <see cref="InvalidOperationException"/> will be thrown.
/// </summary>
/// <exception cref="InvalidOperationException">Start is not called or the transport is closed.</exception>
T Connection { get; }
/// <summary>
/// Create the upper-level connection and start pumping packets from the socket to the upper-level connection.
/// </summary>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException"><see cref="Start"/> has been called before.</exception>
void Start();
}
}

View file

@ -0,0 +1,104 @@
using System.Runtime.CompilerServices;
namespace FurinaImpact.Kcp
{
internal sealed class KcpAcknowledgeList
{
private readonly KcpSendQueue _sendQueue;
private (uint SerialNumber, uint Timestamp)[] _array;
private int _count;
private SpinLock _lock;
public KcpAcknowledgeList(KcpSendQueue sendQueue, int windowSize)
{
_array = new (uint SerialNumber, uint Timestamp)[windowSize];
_count = 0;
_lock = new SpinLock();
_sendQueue = sendQueue;
}
public bool TryGetAt(int index, out uint serialNumber, out uint timestamp)
{
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
if ((uint)index >= (uint)_count)
{
serialNumber = default;
timestamp = default;
return false;
}
(serialNumber, timestamp) = _array[index];
return true;
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
}
public void Clear()
{
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_count = 0;
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
_sendQueue.NotifyAckListChanged(false);
}
public void Add(uint serialNumber, uint timestamp)
{
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
EnsureCapacity();
_array[_count++] = (serialNumber, timestamp);
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
_sendQueue.NotifyAckListChanged(true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity()
{
if (_count == _array.Length)
{
Expand();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void Expand()
{
int capacity = _count + 1;
capacity = Math.Max(capacity + capacity / 2, 16);
var newArray = new (uint SerialNumber, uint Timestamp)[capacity];
_array.AsSpan(0, _count).CopyTo(newArray);
_array = newArray;
}
}
}

View file

@ -0,0 +1,59 @@
using System.Diagnostics;
namespace FurinaImpact.Kcp
{
internal readonly struct KcpBuffer
{
private readonly object _owner;
private readonly Memory<byte> _memory;
private readonly int _length;
public ReadOnlyMemory<byte> DataRegion => _memory.Slice(0, _length);
public int Length => _length;
private KcpBuffer(object owner, Memory<byte> memory, int length)
{
_owner = owner;
_memory = memory;
_length = length;
}
public static KcpBuffer CreateFromSpan(KcpRentedBuffer buffer, ReadOnlySpan<byte> dataSource)
{
Memory<byte> memory = buffer.Memory;
if (dataSource.Length > memory.Length)
{
ThrowRentedBufferTooSmall();
}
dataSource.CopyTo(memory.Span);
return new KcpBuffer(buffer.Owner, memory, dataSource.Length);
}
public KcpBuffer AppendData(ReadOnlySpan<byte> data)
{
if (_length + data.Length > _memory.Length)
{
ThrowRentedBufferTooSmall();
}
data.CopyTo(_memory.Span.Slice(_length));
return new KcpBuffer(_owner, _memory, _length + data.Length);
}
public KcpBuffer Consume(int length)
{
Debug.Assert((uint)length <= (uint)_length);
return new KcpBuffer(_owner, _memory.Slice(length), _length - length);
}
public void Release()
{
new KcpRentedBuffer(_owner, _memory).Dispose();
}
private static void ThrowRentedBufferTooSmall()
{
throw new InvalidOperationException("The rented buffer is not large enough to hold the data.");
}
}
}

View file

@ -0,0 +1,41 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// The options to use when renting buffers from the pool.
/// </summary>
public readonly struct KcpBufferPoolRentOptions : IEquatable<KcpBufferPoolRentOptions>
{
private readonly int _size;
private readonly bool _isOutbound;
/// <summary>
/// The minimum size of the buffer.
/// </summary>
public int Size => _size;
/// <summary>
/// True if the buffer may be passed to the outside of KcpSharp. False if the buffer is only used internally in KcpSharp.
/// </summary>
public bool IsOutbound => _isOutbound;
/// <summary>
/// Create a <see cref="KcpBufferPoolRentOptions"/> with the specified parameters.
/// </summary>
/// <param name="size">The minimum size of the buffer.</param>
/// <param name="isOutbound">True if the buffer may be passed to the outside of KcpSharp. False if the buffer is only used internally in KcpSharp.</param>
public KcpBufferPoolRentOptions(int size, bool isOutbound)
{
_size = size;
_isOutbound = isOutbound;
}
/// <inheritdoc />
public bool Equals(KcpBufferPoolRentOptions other) => _size == other._size && _isOutbound == other.IsOutbound;
/// <inheritdoc />
public override bool Equals(object obj) => obj is KcpBufferPoolRentOptions other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(_size, _isOutbound);
}
}

View file

@ -0,0 +1,10 @@
namespace FurinaImpact.Kcp
{
internal enum KcpCommand : byte
{
Push = 81,
Ack = 82,
WindowProbe = 83,
WindowSize = 84
}
}

View file

@ -0,0 +1,275 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks.Sources;
namespace FurinaImpact.Kcp
{
partial class KcpConversation
{
#if NET6_0_OR_GREATER
[ThreadStatic]
private static KcpConversation? s_currentObject;
private object? _flushStateMachine;
struct KcpFlushAsyncMethodBuilder
{
private readonly KcpConversation _conversation;
private StateMachineBox? _task;
private static readonly StateMachineBox s_syncSuccessSentinel = new SyncSuccessSentinelStateMachineBox();
public KcpFlushAsyncMethodBuilder(KcpConversation conversation)
{
_conversation = conversation;
_task = null;
}
public static KcpFlushAsyncMethodBuilder Create()
{
KcpConversation? conversation = s_currentObject;
Debug.Assert(conversation is not null);
s_currentObject = null;
return new KcpFlushAsyncMethodBuilder(conversation);
}
#pragma warning disable CA1822 // Mark members as static
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
#pragma warning restore CA1822 // Mark members as static
{
Debug.Assert(stateMachine is not null);
stateMachine.MoveNext();
}
public ValueTask Task
{
get
{
if (ReferenceEquals(_task, s_syncSuccessSentinel))
{
return default;
}
StateMachineBox stateMachineBox = _task ??= CreateWeaklyTypedStateMachineBox();
return new ValueTask(stateMachineBox, stateMachineBox.Version);
}
}
#pragma warning disable CA1822 // Mark members as static
public void SetStateMachine(IAsyncStateMachine stateMachine)
#pragma warning restore CA1822 // Mark members as static
{
Debug.Fail("SetStateMachine should not be used.");
}
public void SetResult()
{
if (_task == null)
{
_task = s_syncSuccessSentinel;
}
else
{
_task.SetResult();
}
}
public void SetException(Exception exception)
{
SetException(exception, ref _task);
}
private static void SetException(Exception exception, ref StateMachineBox? boxFieldRef)
{
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
(boxFieldRef ??= CreateWeaklyTypedStateMachineBox()).SetException(exception);
}
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine
{
AwaitOnCompleted(ref awaiter, ref stateMachine, ref _task, _conversation);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref _task, _conversation);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine, ref StateMachineBox? boxRef, KcpConversation conversation) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine
{
StateMachineBox stateMachineBox = GetStateMachineBox(ref stateMachine, ref boxRef, conversation);
AwaitUnsafeOnCompleted(ref awaiter, stateMachineBox);
}
private static void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine, ref StateMachineBox? box, KcpConversation conversation) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine
{
try
{
awaiter.OnCompleted(GetStateMachineBox(ref stateMachine, ref box, conversation).MoveNextAction);
}
catch (Exception exception)
{
var edi = ExceptionDispatchInfo.Capture(exception);
ThreadPool.QueueUserWorkItem(static state => ((ExceptionDispatchInfo)state!).Throw(), edi);
}
}
private static void AwaitUnsafeOnCompleted<TAwaiter>(ref TAwaiter awaiter, StateMachineBox box) where TAwaiter : ICriticalNotifyCompletion
{
try
{
awaiter.UnsafeOnCompleted(box.MoveNextAction);
}
catch (Exception exception)
{
var edi = ExceptionDispatchInfo.Capture(exception);
ThreadPool.QueueUserWorkItem(static state => ((ExceptionDispatchInfo)state!).Throw(), edi);
}
}
private static StateMachineBox CreateWeaklyTypedStateMachineBox()
{
return new StateMachineBox<IAsyncStateMachine>(null);
}
private static StateMachineBox GetStateMachineBox<TStateMachine>(ref TStateMachine stateMachine, ref StateMachineBox? boxFieldRef, KcpConversation conversation) where TStateMachine : IAsyncStateMachine
{
StateMachineBox<TStateMachine>? stateMachineBox = boxFieldRef as StateMachineBox<TStateMachine>;
if (stateMachineBox != null)
{
return stateMachineBox;
}
StateMachineBox<IAsyncStateMachine>? stateMachineBox2 = boxFieldRef as StateMachineBox<IAsyncStateMachine>;
if (stateMachineBox2 != null)
{
if (stateMachineBox2.StateMachine == null)
{
Debugger.NotifyOfCrossThreadDependency();
stateMachineBox2.StateMachine = stateMachine;
}
return stateMachineBox2;
}
Debugger.NotifyOfCrossThreadDependency();
StateMachineBox<TStateMachine> stateMachineBox3 = (StateMachineBox<TStateMachine>)(boxFieldRef = StateMachineBox<TStateMachine>.GetOrCreateBox(conversation));
stateMachineBox3.StateMachine = stateMachine;
return stateMachineBox3;
}
abstract class StateMachineBox : IValueTaskSource
{
protected ManualResetValueTaskSourceCore<bool> _mrvtsc;
protected Action? _moveNextAction;
public virtual Action MoveNextAction => _moveNextAction!;
public short Version => _mrvtsc.Version;
public void SetResult()
{
_mrvtsc.SetResult(true);
}
public void SetException(Exception error)
{
_mrvtsc.SetException(error);
}
public ValueTaskSourceStatus GetStatus(short token)
{
return _mrvtsc.GetStatus(token);
}
public void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags)
{
_mrvtsc.OnCompleted(continuation, state, token, flags);
}
void IValueTaskSource.GetResult(short token)
{
throw new NotSupportedException();
}
}
sealed class SyncSuccessSentinelStateMachineBox : StateMachineBox
{
public SyncSuccessSentinelStateMachineBox()
{
SetResult();
}
}
sealed class StateMachineBox<TStateMachine> : StateMachineBox, IValueTaskSource where TStateMachine : IAsyncStateMachine
{
[MaybeNull]
public TStateMachine StateMachine;
private KcpConversation? _conversation;
public override Action MoveNextAction => _moveNextAction ??= MoveNext;
internal StateMachineBox(KcpConversation? conversation)
{
_conversation = conversation;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static StateMachineBox<TStateMachine> GetOrCreateBox(KcpConversation conversation)
{
if (conversation._flushStateMachine is StateMachineBox<TStateMachine> stateMachine)
{
stateMachine._conversation = conversation;
conversation._flushStateMachine = null;
return stateMachine;
}
return new StateMachineBox<TStateMachine>(conversation);
}
void IValueTaskSource.GetResult(short token)
{
try
{
_mrvtsc.GetResult(token);
}
finally
{
ReturnOrDropBox();
}
}
public void MoveNext()
{
if (StateMachine is not null)
{
StateMachine.MoveNext();
}
}
private void ReturnOrDropBox()
{
StateMachine = default!;
_mrvtsc.Reset();
if (_conversation is not null)
{
_conversation._flushStateMachine = this;
_conversation = null;
}
}
}
}
#endif
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// Options used to control the behaviors of <see cref="KcpConversation"/>.
/// </summary>
public class KcpConversationOptions
{
/// <summary>
/// The buffer pool to rent buffer from.
/// </summary>
public IKcpBufferPool BufferPool { get; set; }
/// <summary>
/// The maximum packet size that can be transmitted over the underlying transport.
/// </summary>
public int Mtu { get; set; } = 1400;
/// <summary>
/// The number of packets in the send window.
/// </summary>
public int SendWindow { get; set; } = 32;
/// <summary>
/// The number of packets in the receive window.
/// </summary>
public int ReceiveWindow { get; set; } = 128;
/// <summary>
/// The nuber of packets in the receive window of the remote host.
/// </summary>
public int RemoteReceiveWindow { get; set; } = 128;
/// <summary>
/// The interval in milliseconds to update the internal state of <see cref="KcpConversation"/>.
/// </summary>
public int UpdateInterval { get; set; } = 100;
/// <summary>
/// Wether no-delay mode is enabled.
/// </summary>
public bool NoDelay { get; set; }
/// <summary>
/// The number of ACK packet skipped before a resend is triggered.
/// </summary>
public int FastResend { get; set; }
/// <summary>
/// Whether congestion control is disabled.
/// </summary>
public bool DisableCongestionControl { get; set; }
/// <summary>
/// Whether stream mode is enabled.
/// </summary>
public bool StreamMode { get; set; }
/// <summary>
/// The number of packets in the send queue.
/// </summary>
public int SendQueueSize { get; set; }
/// <summary>
/// The number of packets in the receive queue.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// The number of bytes to reserve at the start of buffer passed into the underlying transport. The transport should fill this reserved space.
/// </summary>
public int PreBufferSize { get; set; }
/// <summary>
/// The number of bytes to reserve at the end of buffer passed into the underlying transport. The transport should fill this reserved space.
/// </summary>
public int PostBufferSize { get; set; }
/// <summary>
/// Options for customized keep-alive functionality.
/// </summary>
public KcpKeepAliveOptions KeepAliveOptions { get; set; }
/// <summary>
/// Options for receive window size notification functionality.
/// </summary>
public KcpReceiveWindowNotificationOptions ReceiveWindowNotificationOptions { get; set; }
internal const int MtuDefaultValue = 1400;
internal const uint SendWindowDefaultValue = 32;
internal const uint ReceiveWindowDefaultValue = 128;
internal const uint RemoteReceiveWindowDefaultValue = 128;
internal const uint UpdateIntervalDefaultValue = 100;
internal const int SendQueueSizeDefaultValue = 32;
internal const int ReceiveQueueSizeDefaultValue = 32;
}
}

View file

@ -0,0 +1,61 @@
using System.Globalization;
namespace FurinaImpact.Kcp
{
/// <summary>
/// The result of a receive or peek operation.
/// </summary>
public readonly struct KcpConversationReceiveResult : IEquatable<KcpConversationReceiveResult>
{
private readonly int _bytesReceived;
private readonly bool _connectionAlive;
/// <summary>
/// The number of bytes received.
/// </summary>
public int BytesReceived => _bytesReceived;
/// <summary>
/// Whether the underlying transport is marked as closed.
/// </summary>
public bool TransportClosed => !_connectionAlive;
/// <summary>
/// Construct a <see cref="KcpConversationReceiveResult"/> with the specified number of bytes received.
/// </summary>
/// <param name="bytesReceived">The number of bytes received.</param>
public KcpConversationReceiveResult(int bytesReceived)
{
_bytesReceived = bytesReceived;
_connectionAlive = true;
}
/// <summary>
/// Checks whether the two instance is equal.
/// </summary>
/// <param name="left">The one instance.</param>
/// <param name="right">The other instance.</param>
/// <returns>Whether the two instance is equal</returns>
public static bool operator ==(KcpConversationReceiveResult left, KcpConversationReceiveResult right) => left.Equals(right);
/// <summary>
/// Checks whether the two instance is not equal.
/// </summary>
/// <param name="left">The one instance.</param>
/// <param name="right">The other instance.</param>
/// <returns>Whether the two instance is not equal</returns>
public static bool operator !=(KcpConversationReceiveResult left, KcpConversationReceiveResult right) => !left.Equals(right);
/// <inheritdoc />
public bool Equals(KcpConversationReceiveResult other) => BytesReceived == other.BytesReceived && TransportClosed == other.TransportClosed;
/// <inheritdoc />
public override bool Equals(object obj) => obj is KcpConversationReceiveResult other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(BytesReceived, TransportClosed);
/// <inheritdoc />
public override string ToString() => _connectionAlive ? _bytesReceived.ToString(CultureInfo.InvariantCulture) : "Transport is closed.";
}
}

View file

@ -0,0 +1,490 @@
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace FurinaImpact.Kcp
{
internal sealed class KcpConversationUpdateActivation : IValueTaskSource<KcpConversationUpdateNotification>, IDisposable
{
private readonly Timer _timer;
private ManualResetValueTaskSourceCore<KcpConversationUpdateNotification> _mrvtsc;
private bool _disposed;
private bool _notificationPending;
private bool _signaled;
private bool _activeWait;
private CancellationToken _cancellationToken;
private CancellationTokenRegistration _cancellationRegistration;
private readonly WaitList _waitList;
ValueTaskSourceStatus IValueTaskSource<KcpConversationUpdateNotification>.GetStatus(short token) => _mrvtsc.GetStatus(token);
void IValueTaskSource<KcpConversationUpdateNotification>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _mrvtsc.OnCompleted(continuation, state, token, flags);
KcpConversationUpdateNotification IValueTaskSource<KcpConversationUpdateNotification>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (this)
{
_signaled = false;
_activeWait = false;
_cancellationRegistration = default;
}
}
}
public KcpConversationUpdateActivation(int interval)
{
_timer = new Timer(state =>
{
var reference = (WeakReference<KcpConversationUpdateActivation>)state!;
if (reference.TryGetTarget(out KcpConversationUpdateActivation target))
{
target.Notify();
}
}, new WeakReference<KcpConversationUpdateActivation>(this), interval, interval);
_mrvtsc = new ManualResetValueTaskSourceCore<KcpConversationUpdateNotification> { RunContinuationsAsynchronously = true };
_waitList = new WaitList(this);
}
public void Notify()
{
if (_disposed)
{
return;
}
lock (this)
{
if (_disposed || _notificationPending)
{
return;
}
if (_activeWait && !_signaled)
{
_signaled = true;
_cancellationToken = default;
_mrvtsc.SetResult(default);
}
else
{
_notificationPending = true;
}
}
}
private void NotifyPacketReceived()
{
lock (this)
{
if (_disposed)
{
return;
}
if (_activeWait && !_signaled)
{
if (_waitList.Occupy(out KcpConversationUpdateNotification notification))
{
_signaled = true;
_cancellationToken = default;
bool timerNotification = _notificationPending;
_notificationPending = false;
_mrvtsc.SetResult(notification.WithTimerNotification(timerNotification));
}
}
}
}
public ValueTask<KcpConversationUpdateNotification> WaitAsync(CancellationToken cancellationToken)
{
short token;
lock (this)
{
if (_disposed)
{
return default;
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<KcpConversationUpdateNotification>(Task.FromCanceled<KcpConversationUpdateNotification>(cancellationToken));
}
if (_activeWait)
{
throw new InvalidOperationException();
}
if (_waitList.Occupy(out KcpConversationUpdateNotification notification))
{
bool timerNotification = _notificationPending;
_notificationPending = false;
return new ValueTask<KcpConversationUpdateNotification>(notification.WithTimerNotification(timerNotification));
}
if (_notificationPending)
{
_notificationPending = false;
return default;
}
_activeWait = true;
Debug.Assert(!_signaled);
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpConversationUpdateActivation)state)!.CancelWaiting(), this);
return new ValueTask<KcpConversationUpdateNotification>(this, token);
}
private void CancelWaiting()
{
lock (this)
{
if (_activeWait && !_signaled)
{
CancellationToken cancellationToken = _cancellationToken;
_signaled = true;
_cancellationToken = default;
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
public ValueTask InputPacketAsync(ReadOnlyMemory<byte> packet, CancellationToken cancellationToken)
{
if (_disposed)
{
return default;
}
return _waitList.InputPacketAsync(packet, cancellationToken);
}
public void Dispose()
{
lock (this)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_activeWait && !_signaled)
{
_signaled = true;
_cancellationToken = default;
_mrvtsc.SetResult(default);
}
}
_timer.Dispose();
_waitList.Dispose();
}
class WaitList : IValueTaskSource, IKcpConversationUpdateNotificationSource, IDisposable
{
private readonly KcpConversationUpdateActivation _parent;
private LinkedList<WaitItem> _list;
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
private bool _available; // activeWait
private bool _occupied;
private bool _signaled;
private bool _disposed;
private ReadOnlyMemory<byte> _packet;
private CancellationToken _cancellationToken;
private CancellationTokenRegistration _cancellationRegistration;
public ReadOnlyMemory<byte> Packet
{
get
{
lock (this)
{
if (_available && _occupied && !_signaled)
{
return _packet;
}
}
return default;
}
}
ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token);
void IValueTaskSource.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _mrvtsc.OnCompleted(continuation, state, token, flags);
void IValueTaskSource.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
_mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (this)
{
_available = false;
_occupied = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
public WaitList(KcpConversationUpdateActivation parent)
{
_parent = parent;
_mrvtsc = new ManualResetValueTaskSourceCore<bool> { RunContinuationsAsynchronously = true };
}
public ValueTask InputPacketAsync(ReadOnlyMemory<byte> packet, CancellationToken cancellationToken)
{
WaitItem waitItem = null;
short token = 0;
lock (this)
{
if (_disposed)
{
return default;
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask(Task.FromCanceled(cancellationToken));
}
if (_available)
{
waitItem = new WaitItem(this, packet, cancellationToken);
_list ??= new LinkedList<WaitItem>();
_list.AddLast(waitItem.Node);
}
else
{
token = _mrvtsc.Version;
_available = true;
Debug.Assert(!_occupied);
Debug.Assert(!_signaled);
_packet = packet;
_cancellationToken = cancellationToken;
}
}
ValueTask task;
if (waitItem is null)
{
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((WaitList)state)!.CancelWaiting(), this);
task = new ValueTask(this, token);
}
else
{
waitItem.RegisterCancellationToken();
task = new ValueTask(waitItem.Task);
}
_parent.NotifyPacketReceived();
return task;
}
private void CancelWaiting()
{
lock (this)
{
if (_available && !_occupied && !_signaled)
{
_signaled = true;
CancellationToken cancellationToken = _cancellationToken;
_packet = default;
_cancellationToken = default;
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
public bool Occupy(out KcpConversationUpdateNotification notification)
{
lock (this)
{
if (_disposed)
{
notification = default;
return false;
}
if (_available && !_occupied && !_signaled)
{
_occupied = true;
notification = new KcpConversationUpdateNotification(this, true);
return true;
}
if (_list is null)
{
notification = default;
return false;
}
LinkedListNode<WaitItem> node = _list.First;
if (node is not null)
{
_list.Remove(node);
notification = new KcpConversationUpdateNotification(node.Value, true);
return true;
}
}
notification = default;
return false;
}
public void Release()
{
lock (this)
{
if (_available && _occupied && !_signaled)
{
_signaled = true;
_packet = default;
_cancellationToken = default;
_mrvtsc.SetResult(true);
}
}
}
internal bool TryRemove(WaitItem item)
{
lock (this)
{
LinkedList<WaitItem> list = _list;
if (list is null)
{
return false;
}
LinkedListNode<WaitItem> node = item.Node;
if (node.Previous is null && node.Next is null)
{
return false;
}
list.Remove(node);
return true;
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
lock (this)
{
_disposed = true;
if (_available && !_occupied && !_signaled)
{
_signaled = true;
_packet = default;
_cancellationToken = default;
_mrvtsc.SetResult(false);
}
LinkedList<WaitItem> list = _list;
if (list is not null)
{
_list = null;
LinkedListNode<WaitItem> node = list.First;
LinkedListNode<WaitItem> next = node?.Next;
while (node is not null)
{
node.Value.Release();
list.Remove(node);
node = next;
next = node?.Next;
}
}
}
}
}
class WaitItem : TaskCompletionSource, IKcpConversationUpdateNotificationSource
{
private readonly WaitList _parent;
private ReadOnlyMemory<byte> _packet;
private CancellationToken _cancellationToken;
private CancellationTokenRegistration _cancellationRegistration;
private bool _released;
public LinkedListNode<WaitItem> Node { get; }
public ReadOnlyMemory<byte> Packet
{
get
{
lock (this)
{
if (!_released)
{
return _packet;
}
}
return default;
}
}
public WaitItem(WaitList parent, ReadOnlyMemory<byte> packet, CancellationToken cancellationToken)
{
_parent = parent;
_packet = packet;
_cancellationToken = cancellationToken;
Node = new LinkedListNode<WaitItem>(this);
}
public void RegisterCancellationToken()
{
_cancellationRegistration = _cancellationToken.UnsafeRegister(state => ((WaitItem)state)!.CancelWaiting(), this);
}
private void CancelWaiting()
{
CancellationTokenRegistration cancellationRegistration;
if (_parent.TryRemove(this))
{
CancellationToken cancellationToken;
lock (this)
{
_released = true;
cancellationToken = _cancellationToken;
cancellationRegistration = _cancellationRegistration;
_packet = default;
_cancellationToken = default;
_cancellationRegistration = default;
}
TrySetCanceled(cancellationToken);
}
_cancellationRegistration.Dispose();
}
public void Release()
{
CancellationTokenRegistration cancellationRegistration;
lock (this)
{
_released = true;
cancellationRegistration = _cancellationRegistration;
_packet = default;
_cancellationToken = default;
_cancellationRegistration = default;
}
TrySetResult();
cancellationRegistration.Dispose();
}
}
}
}

View file

@ -0,0 +1,30 @@
namespace FurinaImpact.Kcp
{
internal readonly struct KcpConversationUpdateNotification : IDisposable
{
private readonly IKcpConversationUpdateNotificationSource _source;
private readonly bool _skipTimerNotification;
public ReadOnlyMemory<byte> Packet => _source?.Packet ?? default;
public bool TimerNotification => !_skipTimerNotification;
public KcpConversationUpdateNotification(IKcpConversationUpdateNotificationSource source, bool skipTimerNotification)
{
_source = source;
_skipTimerNotification = skipTimerNotification;
}
public KcpConversationUpdateNotification WithTimerNotification(bool timerNotification)
{
return new KcpConversationUpdateNotification(_source, !_skipTimerNotification | timerNotification);
}
public void Dispose()
{
if (_source is not null)
{
_source.Release();
}
}
}
}

View file

@ -0,0 +1,134 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// Helper methods for <see cref="IKcpExceptionProducer{T}"/>.
/// </summary>
public static class KcpExceptionProducerExtensions
{
/// <summary>
/// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Func<Exception, T, bool> handler)
{
if (producer is null)
{
throw new ArgumentNullException(nameof(producer));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
producer.SetExceptionHandler(
(ex, conv, state) => ((Func<Exception, T, bool>)state)!.Invoke(ex, conv),
handler
);
}
/// <summary>
/// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Func<Exception, bool> handler)
{
if (producer is null)
{
throw new ArgumentNullException(nameof(producer));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
producer.SetExceptionHandler(
(ex, conv, state) => ((Func<Exception, bool>)state)!.Invoke(ex),
handler
);
}
/// <summary>
/// Set the handler to invoke when exception is thrown.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
/// <param name="state">The state object to pass into the exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Action<Exception, T, object> handler, object state)
{
if (producer is null)
{
throw new ArgumentNullException(nameof(producer));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
producer.SetExceptionHandler(
(ex, conv, state) =>
{
var tuple = (Tuple<Action<Exception, T, object>, object>)state!;
tuple.Item1.Invoke(ex, conv, tuple.Item2);
return false;
},
Tuple.Create(handler, state)
);
}
/// <summary>
/// Set the handler to invoke when exception is thrown.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Action<Exception, T> handler)
{
if (producer is null)
{
throw new ArgumentNullException(nameof(producer));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
producer.SetExceptionHandler(
(ex, conv, state) =>
{
var handler = (Action<Exception, T>)state!;
handler.Invoke(ex, conv);
return false;
},
handler
);
}
/// <summary>
/// Set the handler to invoke when exception is thrown.
/// </summary>
/// <param name="producer">The producer instance.</param>
/// <param name="handler">The exception handler.</param>
public static void SetExceptionHandler<T>(this IKcpExceptionProducer<T> producer, Action<Exception> handler)
{
if (producer is null)
{
throw new ArgumentNullException(nameof(producer));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
producer.SetExceptionHandler(
(ex, conv, state) =>
{
var handler = (Action<Exception>)state!;
handler.Invoke(ex);
return false;
},
handler
);
}
}
}

View file

@ -0,0 +1,14 @@
namespace FurinaImpact.Kcp
{
internal static class KcpGlobalVars
{
#if !CONVID32
public const ushort CONVID_LENGTH = 8;
public const ushort HEADER_LENGTH_WITH_CONVID = 28;
public const ushort HEADER_LENGTH_WITHOUT_CONVID = 20;
#else
public const ushort HEADER_LENGTH_WITH_CONVID = 24;
public const ushort HEADER_LENGTH_WITHOUT_CONVID = 20;
#endif
}
}

View file

@ -0,0 +1,30 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// Options for customized keep-alive functionality.
/// </summary>
public sealed class KcpKeepAliveOptions
{
/// <summary>
/// Create an instance of option object for customized keep-alive functionality.
/// </summary>
/// <param name="sendInterval">The minimum interval in milliseconds between sending keep-alive messages.</param>
/// <param name="gracePeriod">When no packets are received during this period (in milliseconds), the transport is considered to be closed.</param>
public KcpKeepAliveOptions(int sendInterval, int gracePeriod)
{
if (sendInterval <= 0)
{
throw new ArgumentOutOfRangeException(nameof(sendInterval));
}
if (gracePeriod <= 0)
{
throw new ArgumentOutOfRangeException(nameof(gracePeriod));
}
SendInterval = sendInterval;
GracePeriod = gracePeriod;
}
internal int SendInterval { get; }
internal int GracePeriod { get; }
}
}

View file

@ -0,0 +1,339 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
namespace FurinaImpact.Kcp
{
/// <summary>
/// Multiplex many channels or conversations over the same transport.
/// </summary>
/// <typeparam name="T">The state of the channel.</typeparam>
public sealed class KcpMultiplexConnection<T> : IKcpTransport, IKcpConversation, IKcpMultiplexConnection<T>
{
private readonly IKcpTransport _transport;
private readonly ConcurrentDictionary<long, (IKcpConversation Conversation, T? State)> _conversations = new();
private bool _transportClosed;
private bool _disposed;
private readonly Action<T?> _disposeAction;
/// <summary>
/// Construct a multiplexed connection over a transport.
/// </summary>
/// <param name="transport">The underlying transport.</param>
public KcpMultiplexConnection(IKcpTransport transport)
{
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
_disposeAction = null;
}
/// <summary>
/// Construct a multiplexed connection over a transport.
/// </summary>
/// <param name="transport">The underlying transport.</param>
/// <param name="disposeAction">The action to invoke when state object is removed.</param>
public KcpMultiplexConnection(IKcpTransport transport, Action<T?> disposeAction)
{
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
_disposeAction = disposeAction;
}
private void CheckDispose()
{
if (_disposed)
{
ThrowObjectDisposedException();
}
}
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(KcpMultiplexConnection<T>));
}
/// <summary>
/// Process a newly received packet from the transport.
/// </summary>
/// <param name="packet">The content of the packet with conversation ID.</param>
/// <param name="cancellationToken">A token to cancel this operation.</param>
/// <returns>A <see cref="ValueTask"/> that completes when the packet is handled by the corresponding channel or conversation.</returns>
public ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken = default)
{
ReadOnlySpan<byte> span = packet.Buffer.AsSpan();
if (span.Length < KcpGlobalVars.CONVID_LENGTH)
{
return default;
}
if (_transportClosed || _disposed)
{
return default;
}
var id = BinaryPrimitives.ReadInt64BigEndian(span);
if (_conversations.TryGetValue(id, out (IKcpConversation Conversation, T? State) value))
{
return value.Conversation.InputPakcetAsync(packet, cancellationToken);
}
return default;
}
/// <summary>
/// Determine whether the multiplex connection contains a conversation with the specified id.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <returns>True if the multiplex connection contains the specified conversation. Otherwise false.</returns>
public bool Contains(long id)
{
CheckDispose();
return _conversations.ContainsKey(id);
}
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="options">The options of the <see cref="KcpRawChannel"/>.</param>
/// <returns>The raw channel created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, KcpRawChannelOptions options = null)
{
KcpRawChannel channel = new KcpRawChannel(remoteEndpoint, this, id, options);
try
{
RegisterConversation(channel, id, default);
if (_transportClosed)
{
channel.SetTransportClosed();
}
return Interlocked.Exchange(ref channel, null)!;
}
finally
{
if (channel is not null)
{
channel.Dispose();
}
}
}
/// <summary>
/// Create a raw channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="state">The user state of this channel.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel"/>.</param>
/// <returns>The raw channel created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, T state, KcpRawChannelOptions options = null)
{
var channel = new KcpRawChannel(remoteEndpoint, this, id, options);
try
{
RegisterConversation(channel, id, state);
if (_transportClosed)
{
channel.SetTransportClosed();
}
return Interlocked.Exchange(ref channel, null)!;
}
finally
{
if (channel is not null)
{
channel.Dispose();
}
}
}
/// <summary>
/// Create a conversation with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="options">The options of the <see cref="KcpConversation"/>.</param>
/// <returns>The KCP conversation created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, KcpConversationOptions options = null)
{
var conversation = new KcpConversation(remoteEndpoint, this, id, options);
try
{
RegisterConversation(conversation, id, default);
if (_transportClosed)
{
conversation.SetTransportClosed();
}
return Interlocked.Exchange(ref conversation, null)!;
}
finally
{
if (conversation is not null)
{
conversation.Dispose();
}
}
}
/// <summary>
/// Create a conversation with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="remoteEndpoint">The remote Endpoint</param>
/// <param name="state">The user state of this conversation.</param>
/// <param name="options">The options of the <see cref="KcpConversation"/>.</param>
/// <returns>The KCP conversation created.</returns>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, T state, KcpConversationOptions options = null)
{
var conversation = new KcpConversation(remoteEndpoint, this, id, options);
try
{
RegisterConversation(conversation, id, state);
if (_transportClosed)
{
conversation.SetTransportClosed();
}
return Interlocked.Exchange(ref conversation, null)!;
}
finally
{
if (conversation is not null)
{
conversation.Dispose();
}
}
}
/// <summary>
/// Register a conversation or channel with the specified conversation ID and user state.
/// </summary>
/// <param name="conversation">The conversation or channel to register.</param>
/// <param name="id">The conversation ID.</param>
/// <exception cref="ArgumentNullException"><paramref name="conversation"/> is not provided.</exception>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public void RegisterConversation(IKcpConversation conversation, long id)
=> RegisterConversation(conversation, id, default);
/// <summary>
/// Register a conversation or channel with the specified conversation ID and user state.
/// </summary>
/// <param name="conversation">The conversation or channel to register.</param>
/// <param name="id">The conversation ID.</param>
/// <param name="state">The user state</param>
/// <exception cref="ArgumentNullException"><paramref name="conversation"/> is not provided.</exception>
/// <exception cref="ObjectDisposedException">The current instance is disposed.</exception>
/// <exception cref="InvalidOperationException">Another channel or conversation with the same ID was already registered.</exception>
public void RegisterConversation(IKcpConversation conversation, long id, T? state)
{
if (conversation is null)
{
throw new ArgumentNullException(nameof(conversation));
}
CheckDispose();
(IKcpConversation addedConversation, T? _) = _conversations.GetOrAdd(id, (conversation, state));
if (!ReferenceEquals(addedConversation, conversation))
{
throw new InvalidOperationException("Duplicated conversation.");
}
if (_disposed)
{
if (_conversations.TryRemove(id, out (IKcpConversation Conversation, T? State) value) && _disposeAction is not null)
{
_disposeAction.Invoke(value.State);
}
ThrowObjectDisposedException();
}
}
/// <summary>
/// Unregister a conversation or channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <returns>The conversation unregistered. Returns null when the conversation with the specified ID is not found.</returns>
public IKcpConversation UnregisterConversation(long id)
{
return UnregisterConversation(id, out _);
}
/// <summary>
/// Unregister a conversation or channel with the specified conversation ID.
/// </summary>
/// <param name="id">The conversation ID.</param>
/// <param name="state">The user state.</param>
/// <returns>The conversation unregistered. Returns null when the conversation with the specified ID is not found.</returns>
public IKcpConversation UnregisterConversation(long id, out T? state)
{
if (!_transportClosed && !_disposed && _conversations.TryRemove(id, out (IKcpConversation Conversation, T? State) value))
{
value.Conversation.SetTransportClosed();
state = value.State;
if (_disposeAction is not null)
{
_disposeAction.Invoke(state);
}
return value.Conversation;
}
state = default;
return default;
}
/// <inheritdoc />
public ValueTask SendPacketAsync(Memory<byte> packet, IPEndPoint remoteEndpoint, CancellationToken cancellationToken = default)
{
if (_transportClosed || _disposed)
{
return default;
}
return _transport.SendPacketAsync(packet, remoteEndpoint, cancellationToken);
}
/// <inheritdoc />
public void SetTransportClosed()
{
_transportClosed = true;
foreach ((IKcpConversation conversation, T? _) in _conversations.Values)
{
conversation.SetTransportClosed();
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
_transportClosed = true;
_disposed = true;
while (!_conversations.IsEmpty)
{
foreach (var id in _conversations.Keys)
{
if (_conversations.TryRemove(id, out (IKcpConversation Conversation, T? State) value))
{
value.Conversation.Dispose();
if (_disposeAction is not null)
{
_disposeAction.Invoke(value.State);
}
}
}
}
}
public void SetCallbacks(int handshakeSize, Func<UdpReceiveResult, ValueTask> handshakeHandler)
{
throw new NotSupportedException();
}
}
}

View file

@ -0,0 +1,75 @@
using System.Buffers.Binary;
using System.Diagnostics;
namespace FurinaImpact.Kcp
{
internal readonly struct KcpPacketHeader : IEquatable<KcpPacketHeader>
{
public KcpPacketHeader(KcpCommand command, byte fragment, ushort windowSize, uint timestamp, uint serialNumber, uint unacknowledged)
{
Command = command;
Fragment = fragment;
WindowSize = windowSize;
Timestamp = timestamp;
SerialNumber = serialNumber;
Unacknowledged = unacknowledged;
}
internal KcpPacketHeader(byte fragment)
{
Command = 0;
Fragment = fragment;
WindowSize = 0;
Timestamp = 0;
SerialNumber = 0;
Unacknowledged = 0;
}
public KcpCommand Command { get; }
public byte Fragment { get; }
public ushort WindowSize { get; }
public uint Timestamp { get; }
public uint SerialNumber { get; }
public uint Unacknowledged { get; }
public bool Equals(KcpPacketHeader other) => Command == other.Command && Fragment == other.Fragment && WindowSize == other.WindowSize && Timestamp == other.Timestamp && SerialNumber == other.SerialNumber && Unacknowledged == other.Unacknowledged;
public override bool Equals(object obj) => obj is KcpPacketHeader other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Command, Fragment, WindowSize, Timestamp, SerialNumber, Unacknowledged);
public static KcpPacketHeader Parse(ReadOnlySpan<byte> buffer)
{
Debug.Assert(buffer.Length >= 16);
return new KcpPacketHeader(
(KcpCommand)buffer[0],
buffer[1],
BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)),
BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(4)),
BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(8)),
BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(12))
);
}
internal void EncodeHeader(ulong? conversationId, int payloadLength, Span<byte> destination, out int bytesWritten)
{
Debug.Assert(destination.Length >= 20);
if (conversationId.HasValue)
{
BinaryPrimitives.WriteUInt64BigEndian(destination, conversationId.GetValueOrDefault());
destination = destination.Slice(8);
bytesWritten = 28;
}
else
{
bytesWritten = 20;
}
Debug.Assert(destination.Length >= 20);
destination[1] = Fragment;
destination[0] = (byte)Command;
BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(2), WindowSize);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4), Timestamp);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8), SerialNumber);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12), Unacknowledged);
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(16), (uint)payloadLength);
}
}
}

View file

@ -0,0 +1,10 @@
namespace FurinaImpact.Kcp
{
[Flags]
internal enum KcpProbeType
{
None = 0,
AskSend = 1,
AskTell = 2,
}
}

View file

@ -0,0 +1,371 @@
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
namespace FurinaImpact.Kcp
{
/// <summary>
/// An unreliable channel with a conversation ID.
/// </summary>
public sealed class KcpRawChannel : IKcpConversation, IKcpExceptionProducer<KcpRawChannel>
{
private readonly IKcpBufferPool _bufferPool;
private readonly IKcpTransport _transport;
private readonly IPEndPoint _remoteEndPoint;
private readonly ulong? _id;
private readonly int _mtu;
private readonly int _preBufferSize;
private readonly int _postBufferSize;
private CancellationTokenSource _sendLoopCts;
private readonly KcpRawReceiveQueue _receiveQueue;
private readonly KcpRawSendOperation _sendOperation;
private readonly AsyncAutoResetEvent<int> _sendNotification;
private Func<Exception, KcpRawChannel, object, bool> _exceptionHandler;
private object _exceptionHandlerState;
/// <summary>
/// Construct a unreliable channel with a conversation ID.
/// </summary>
/// <param name="remoteEndPoint">The remote Endpoint</param>
/// <param name="transport">The underlying transport.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel"/>.</param>
public KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, KcpRawChannelOptions options)
: this(remoteEndPoint, transport, null, options)
{ }
/// <summary>
/// Construct a unreliable channel with a conversation ID.
/// </summary>
/// <param name="remoteEndPoint">The remote Endpoint</param>
/// <param name="transport">The underlying transport.</param>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel"/>.</param>
public KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, long conversationId, KcpRawChannelOptions options)
: this(remoteEndPoint, transport, (ulong)conversationId, options)
{ }
private KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, ulong? conversationId, KcpRawChannelOptions options)
{
_bufferPool = options?.BufferPool ?? DefaultArrayPoolBufferAllocator.Default;
_remoteEndPoint = remoteEndPoint;
_transport = transport;
_id = conversationId;
if (options is null)
{
_mtu = KcpConversationOptions.MtuDefaultValue;
}
else if (options.Mtu < 50)
{
throw new ArgumentException("MTU must be at least 50.", nameof(options));
}
else
{
_mtu = options.Mtu;
}
_preBufferSize = options?.PreBufferSize ?? 0;
_postBufferSize = options?.PostBufferSize ?? 0;
if (_preBufferSize < 0)
{
throw new ArgumentException("PreBufferSize must be a non-negative integer.", nameof(options));
}
if (_postBufferSize < 0)
{
throw new ArgumentException("PostBufferSize must be a non-negative integer.", nameof(options));
}
if ((uint)(_preBufferSize + _postBufferSize) >= (uint)_mtu)
{
throw new ArgumentException("The sum of PreBufferSize and PostBufferSize must be less than MTU.", nameof(options));
}
if (conversationId.HasValue && (uint)(_preBufferSize + _postBufferSize) >= (uint)(_mtu - 4))
{
throw new ArgumentException("The sum of PreBufferSize and PostBufferSize is too large. There is not enough space in the packet for the conversation ID.", nameof(options));
}
int queueSize = options?.ReceiveQueueSize ?? 32;
if (queueSize < 1)
{
throw new ArgumentException("QueueSize must be a positive integer.", nameof(options));
}
_sendLoopCts = new CancellationTokenSource();
_sendNotification = new AsyncAutoResetEvent<int>();
_receiveQueue = new KcpRawReceiveQueue(_bufferPool, queueSize);
_sendOperation = new KcpRawSendOperation(_sendNotification);
RunSendLoop();
}
/// <summary>
/// Set the handler to invoke when exception is thrown during flushing packets to the transport. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation and mark the transport as closed.
/// </summary>
/// <param name="handler">The exception handler.</param>
/// <param name="state">The state object to pass into the exception handler.</param>
public void SetExceptionHandler(Func<Exception, KcpRawChannel, object, bool> handler, object state)
{
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
_exceptionHandler = handler;
_exceptionHandlerState = state;
}
/// <summary>
/// Get the ID of the current conversation.
/// </summary>
public long? ConversationId => (long?)_id;
/// <summary>
/// Get whether the transport is marked as closed.
/// </summary>
public bool TransportClosed => _sendLoopCts is null;
/// <summary>
/// Send message to the underlying transport.
/// </summary>
/// <param name="buffer">The content of the message</param>
/// <param name="cancellationToken">The token to cancel this operation.</param>
/// <exception cref="ArgumentException">The size of the message is larger than mtu, thus it can not be sent.</exception>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> is fired before send operation is completed.</exception>
/// <exception cref="InvalidOperationException">The send operation is initiated concurrently.</exception>
/// <exception cref="ObjectDisposedException">The <see cref="KcpConversation"/> instance is disposed.</exception>
/// <returns>A <see cref="ValueTask{Boolean}"/> that completes when the entire message is put into the queue. The result of the task is false when the transport is closed.</returns>
public ValueTask<bool> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
=> _sendOperation.SendAsync(buffer, cancellationToken);
/// <summary>
/// Cancel the current send operation or flush operation.
/// </summary>
/// <returns>True if the current operation is canceled. False if there is no active send operation.</returns>
public bool CancelPendingSend()
=> _sendOperation.CancelPendingOperation(null, default);
/// <summary>
/// Cancel the current send operation or flush operation.
/// </summary>
/// <param name="innerException">The inner exception of the <see cref="OperationCanceledException"/> thrown by the <see cref="SendAsync(ReadOnlyMemory{byte}, CancellationToken)"/> method.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> in the <see cref="OperationCanceledException"/> thrown by the <see cref="SendAsync(ReadOnlyMemory{byte}, CancellationToken)"/> method.</param>
/// <returns>True if the current operation is canceled. False if there is no active send operation.</returns>
public bool CancelPendingSend(Exception innerException, CancellationToken cancellationToken)
=> _sendOperation.CancelPendingOperation(innerException, cancellationToken);
private async void RunSendLoop()
{
CancellationToken cancellationToken = _sendLoopCts?.Token ?? new CancellationToken(true);
KcpRawSendOperation sendOperation = _sendOperation;
AsyncAutoResetEvent<int> ev = _sendNotification;
int mss = _mtu - _preBufferSize - _postBufferSize;
if (_id.HasValue)
{
mss -= 8;
}
try
{
while (!cancellationToken.IsCancellationRequested)
{
int payloadSize = await ev.WaitAsync().ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
{
break;
}
if (payloadSize < 0 || payloadSize > mss)
{
_ = sendOperation.TryConsume(default, out _);
continue;
}
int overhead = _preBufferSize + _postBufferSize;
if (_id.HasValue)
{
overhead += 8;
}
{
using KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(payloadSize + overhead, true));
Memory<byte> memory = owner.Memory;
// Fill the buffer
if (_preBufferSize != 0)
{
memory.Span.Slice(0, _preBufferSize).Clear();
memory = memory.Slice(_preBufferSize);
}
if (_id.HasValue)
{
BinaryPrimitives.WriteUInt64LittleEndian(memory.Span, _id.GetValueOrDefault());
memory = memory.Slice(8);
}
if (!sendOperation.TryConsume(memory, out int bytesWritten))
{
continue;
}
payloadSize = Math.Min(payloadSize, bytesWritten);
memory = memory.Slice(payloadSize);
if (_postBufferSize != 0)
{
memory.Span.Slice(0, _postBufferSize).Clear();
}
// Send the buffer
try
{
await _transport.SendPacketAsync(owner.Memory.Slice(0, payloadSize + overhead), _remoteEndPoint, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
if (!HandleFlushException(ex))
{
break;
}
}
}
}
}
catch (OperationCanceledException)
{
// Do nothing
}
catch (Exception ex)
{
HandleFlushException(ex);
}
}
private bool HandleFlushException(Exception ex)
{
Func<Exception, KcpRawChannel, object, bool> handler = _exceptionHandler;
object state = _exceptionHandlerState;
bool result = false;
if (handler is not null)
{
try
{
result = handler.Invoke(ex, this, state);
}
catch
{
result = false;
}
}
if (!result)
{
SetTransportClosed();
}
return result;
}
/// <inheritdoc />
public ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken = default)
{
ReadOnlySpan<byte> span = packet.Buffer.AsSpan();
int overhead = _id.HasValue ? KcpGlobalVars.CONVID_LENGTH : 0;
if (span.Length < overhead || span.Length > _mtu)
{
return default;
}
if (_id.HasValue)
{
if (BinaryPrimitives.ReadUInt64BigEndian(span) != _id.GetValueOrDefault())
{
return default;
}
span = span.Slice(8);
}
_receiveQueue.Enqueue(span);
return default;
}
/// <summary>
/// Get the size of the next available message in the receive queue.
/// </summary>
/// <param name="result">The transport state and the size of the next available message.</param>
/// <exception cref="InvalidOperationException">The receive or peek operation is initiated concurrently.</exception>
/// <returns>True if the receive queue contains at least one message. False if the receive queue is empty or the transport is closed.</returns>
public bool TryPeek(out KcpConversationReceiveResult result)
=> _receiveQueue.TryPeek(out result);
/// <summary>
/// Remove the next available message in the receive queue and copy its content into <paramref name="buffer"/>.
/// </summary>
/// <param name="buffer">The buffer to receive message.</param>
/// <param name="result">The transport state and the count of bytes moved into <paramref name="buffer"/>.</param>
/// <exception cref="ArgumentException">The size of the next available message is larger than the size of <paramref name="buffer"/>.</exception>
/// <exception cref="InvalidOperationException">The receive or peek operation is initiated concurrently.</exception>
/// <returns>True if the next available message is moved into <paramref name="buffer"/>. False if the receive queue is empty or the transport is closed.</returns>
public bool TryReceive(Span<byte> buffer, out KcpConversationReceiveResult result)
=> _receiveQueue.TryReceive(buffer, out result);
/// <summary>
/// Wait until the receive queue contains at least one message.
/// </summary>
/// <param name="cancellationToken">The token to cancel this operation.</param>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> is fired before receive operation is completed.</exception>
/// <exception cref="InvalidOperationException">The receive or peek operation is initiated concurrently.</exception>
/// <returns>A <see cref="ValueTask{KcpConversationReceiveResult}"/> that completes when the receive queue contains at least one full message, or at least one byte in stream mode. Its result contains the transport state and the size of the available message.</returns>
public ValueTask<KcpConversationReceiveResult> WaitToReceiveAsync(CancellationToken cancellationToken)
=> _receiveQueue.WaitToReceiveAsync(cancellationToken);
/// <summary>
/// Wait for the next full message to arrive if the receive queue is empty. Remove the next available message in the receive queue and copy its content into <paramref name="buffer"/>.
/// </summary>
/// <param name="buffer">The buffer to receive message.</param>
/// <param name="cancellationToken">The token to cancel this operation.</param>
/// <exception cref="ArgumentException">The size of the next available message is larger than the size of <paramref name="buffer"/>.</exception>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> is fired before send operation is completed.</exception>
/// <exception cref="InvalidOperationException">The receive or peek operation is initiated concurrently.</exception>
/// <returns>A <see cref="ValueTask{KcpConversationReceiveResult}"/> that completes when a message is moved into <paramref name="buffer"/> or the transport is closed. Its result contains the transport state and the count of bytes written into <paramref name="buffer"/>.</returns>
public ValueTask<KcpConversationReceiveResult> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
=> _receiveQueue.ReceiveAsync(buffer, cancellationToken);
/// <summary>
/// Cancel the current receive operation.
/// </summary>
/// <returns>True if the current operation is canceled. False if there is no active send operation.</returns>
public bool CancelPendingReceive()
=> _receiveQueue.CancelPendingOperation(null, default);
/// <summary>
/// Cancel the current send operation or flush operation.
/// </summary>
/// <param name="innerException">The inner exception of the <see cref="OperationCanceledException"/> thrown by the <see cref="ReceiveAsync(Memory{byte}, CancellationToken)"/> method or <see cref="WaitToReceiveAsync(CancellationToken)"/> method.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> in the <see cref="OperationCanceledException"/> thrown by the <see cref="ReceiveAsync(Memory{byte}, CancellationToken)"/> method or <see cref="WaitToReceiveAsync(CancellationToken)"/> method.</param>
/// <returns>True if the current operation is canceled. False if there is no active send operation.</returns>
public bool CancelPendingReceive(Exception innerException, CancellationToken cancellationToken)
=> _receiveQueue.CancelPendingOperation(innerException, cancellationToken);
/// <inheritdoc />
public void SetTransportClosed()
{
CancellationTokenSource cts = Interlocked.Exchange(ref _sendLoopCts, null);
if (cts is not null)
{
cts.Cancel();
cts.Dispose();
}
_receiveQueue.SetTransportClosed();
_sendOperation.SetTransportClosed();
_sendNotification.Set(0);
}
/// <inheritdoc />
public void Dispose()
{
SetTransportClosed();
_receiveQueue.Dispose();
_sendOperation.Dispose();
}
}
}

View file

@ -0,0 +1,33 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// Options used to control the behaviors of <see cref="KcpRawChannelOptions"/>.
/// </summary>
public sealed class KcpRawChannelOptions
{
/// <summary>
/// The buffer pool to rent buffer from.
/// </summary>
public IKcpBufferPool BufferPool { get; set; }
/// <summary>
/// The maximum packet size that can be transmitted over the underlying transport.
/// </summary>
public int Mtu { get; set; } = 1400;
/// <summary>
/// The number of packets in the receive queue.
/// </summary>
public int ReceiveQueueSize { get; set; } = 32;
/// <summary>
/// The number of bytes to reserve at the start of buffer passed into the underlying transport. The transport should fill this reserved space.
/// </summary>
public int PreBufferSize { get; set; }
/// <summary>
/// The number of bytes to reserve at the end of buffer passed into the underlying transport. The transport should fill this reserved space.
/// </summary>
public int PostBufferSize { get; set; }
}
}

View file

@ -0,0 +1,356 @@
using System.Threading.Tasks.Sources;
using System.Diagnostics;
using FurinaImpact.Kcp;
#if NEED_LINKEDLIST_SHIM
using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<KcpSharp.KcpBuffer>;
using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<KcpSharp.KcpBuffer>;
#else
using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<FurinaImpact.Kcp.KcpBuffer>;
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<FurinaImpact.Kcp.KcpBuffer>;
#endif
namespace FurinaImpact.Kcp
{
internal sealed class KcpRawReceiveQueue : IValueTaskSource<KcpConversationReceiveResult>, IDisposable
{
private ManualResetValueTaskSourceCore<KcpConversationReceiveResult> _mrvtsc;
private readonly IKcpBufferPool _bufferPool;
private readonly int _capacity;
private readonly LinkedListOfQueueItem _queue;
private readonly LinkedListOfQueueItem _recycled;
private bool _transportClosed;
private bool _disposed;
private bool _activeWait;
private bool _signaled;
private bool _bufferProvided;
private Memory<byte> _buffer;
private CancellationToken _cancellationToken;
private CancellationTokenRegistration _cancellationRegistration;
public KcpRawReceiveQueue(IKcpBufferPool bufferPool, int capacity)
{
_bufferPool = bufferPool;
_capacity = capacity;
_queue = new LinkedListOfQueueItem();
_recycled = new LinkedListOfQueueItem();
}
KcpConversationReceiveResult IValueTaskSource<KcpConversationReceiveResult>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
ValueTaskSourceStatus IValueTaskSource<KcpConversationReceiveResult>.GetStatus(short token) => _mrvtsc.GetStatus(token);
void IValueTaskSource<KcpConversationReceiveResult>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _mrvtsc.OnCompleted(continuation, state, token, flags);
public bool TryPeek(out KcpConversationReceiveResult result)
{
lock (_queue)
{
if (_disposed || _transportClosed)
{
result = default;
return false;
}
if (_activeWait)
{
ThrowHelper.ThrowConcurrentReceiveException();
}
LinkedListNodeOfQueueItem first = _queue.First;
if (first is null)
{
result = new KcpConversationReceiveResult(0);
return false;
}
result = new KcpConversationReceiveResult(first.ValueRef.Length);
return true;
}
}
public ValueTask<KcpConversationReceiveResult> WaitToReceiveAsync(CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return default;
}
if (_activeWait)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewConcurrentReceiveException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromCanceled<KcpConversationReceiveResult>(cancellationToken));
}
LinkedListNodeOfQueueItem first = _queue.First;
if (first is not null)
{
return new ValueTask<KcpConversationReceiveResult>(new KcpConversationReceiveResult(first.ValueRef.Length));
}
_activeWait = true;
Debug.Assert(!_signaled);
_bufferProvided = false;
_buffer = default;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpRawReceiveQueue)state)!.SetCanceled(), this);
return new ValueTask<KcpConversationReceiveResult>(this, token);
}
public bool TryReceive(Span<byte> buffer, out KcpConversationReceiveResult result)
{
lock (_queue)
{
if (_disposed || _transportClosed)
{
result = default;
return false;
}
if (_activeWait)
{
ThrowHelper.ThrowConcurrentReceiveException();
}
LinkedListNodeOfQueueItem first = _queue.First;
if (first is null)
{
result = new KcpConversationReceiveResult(0);
return false;
}
ref KcpBuffer source = ref first.ValueRef;
if (buffer.Length < source.Length)
{
ThrowHelper.ThrowBufferTooSmall();
}
source.DataRegion.Span.CopyTo(buffer);
result = new KcpConversationReceiveResult(source.Length);
_queue.RemoveFirst();
source.Release();
source = default;
_recycled.AddLast(first);
return true;
}
}
public ValueTask<KcpConversationReceiveResult> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return default;
}
if (_activeWait)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewConcurrentReceiveException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromCanceled<KcpConversationReceiveResult>(cancellationToken));
}
LinkedListNodeOfQueueItem first = _queue.First;
if (first is not null)
{
ref KcpBuffer source = ref first.ValueRef;
int length = source.Length;
if (buffer.Length < source.Length)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewBufferTooSmallForBufferArgument()));
}
_queue.Remove(first);
source.DataRegion.CopyTo(buffer);
source.Release();
source = default;
_recycled.AddLast(first);
return new ValueTask<KcpConversationReceiveResult>(new KcpConversationReceiveResult(length));
}
_activeWait = true;
Debug.Assert(!_signaled);
_bufferProvided = true;
_buffer = buffer;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpRawReceiveQueue)state)!.SetCanceled(), this);
return new ValueTask<KcpConversationReceiveResult>(this, token);
}
public bool CancelPendingOperation(Exception innerException, CancellationToken cancellationToken)
{
lock (_queue)
{
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewOperationCanceledExceptionForCancelPendingReceive(innerException, cancellationToken));
return true;
}
}
return false;
}
private void SetCanceled()
{
lock (_queue)
{
if (_activeWait && !_signaled)
{
CancellationToken cancellationToken = _cancellationToken;
ClearPreviousOperation();
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
private void ClearPreviousOperation()
{
_signaled = true;
_bufferProvided = false;
_buffer = default;
_cancellationToken = default;
}
public void Enqueue(ReadOnlySpan<byte> buffer)
{
lock (_queue)
{
if (_transportClosed || _disposed)
{
return;
}
int queueSize = _queue.Count;
if (queueSize > 0 || !_activeWait)
{
if (queueSize >= _capacity)
{
return;
}
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false));
_queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer)));
return;
}
if (!_bufferProvided)
{
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false));
_queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer)));
ClearPreviousOperation();
_mrvtsc.SetResult(new KcpConversationReceiveResult(buffer.Length));
return;
}
if (buffer.Length > _buffer.Length)
{
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false));
_queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer)));
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewBufferTooSmallForBufferArgument());
return;
}
buffer.CopyTo(_buffer.Span);
ClearPreviousOperation();
_mrvtsc.SetResult(new KcpConversationReceiveResult(buffer.Length));
}
}
private LinkedListNodeOfQueueItem AllocateNode(KcpBuffer buffer)
{
LinkedListNodeOfQueueItem node = _recycled.First;
if (node is null)
{
node = new LinkedListNodeOfQueueItem(buffer);
}
else
{
node.ValueRef = buffer;
_recycled.Remove(node);
}
return node;
}
public void SetTransportClosed()
{
lock (_queue)
{
if (_transportClosed || _disposed)
{
return;
}
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetResult(default);
}
_recycled.Clear();
_transportClosed = true;
}
}
public void Dispose()
{
lock (_queue)
{
if (_disposed)
{
return;
}
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetResult(default);
}
LinkedListNodeOfQueueItem node = _queue.First;
while (node is not null)
{
node.ValueRef.Release();
node = node.Next;
}
_queue.Clear();
_recycled.Clear();
_disposed = true;
_transportClosed = true;
}
}
}
}

View file

@ -0,0 +1,182 @@
using System.Diagnostics;
using System.Threading.Tasks.Sources;
namespace FurinaImpact.Kcp
{
internal sealed class KcpRawSendOperation : IValueTaskSource<bool>, IDisposable
{
private readonly AsyncAutoResetEvent<int> _notification;
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
private bool _transportClosed;
private bool _disposed;
private bool _activeWait;
private bool _signaled;
private ReadOnlyMemory<byte> _buffer;
private CancellationToken _cancellationToken;
private CancellationTokenRegistration _cancellationRegistration;
public KcpRawSendOperation(AsyncAutoResetEvent<int> notification)
{
_notification = notification;
_mrvtsc = new ManualResetValueTaskSourceCore<bool>()
{
RunContinuationsAsynchronously = true
};
}
bool IValueTaskSource<bool>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (this)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
ValueTaskSourceStatus IValueTaskSource<bool>.GetStatus(short token) => _mrvtsc.GetStatus(token);
void IValueTaskSource<bool>.OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _mrvtsc.OnCompleted(continuation, state, token, flags);
public ValueTask<bool> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
short token;
lock (this)
{
if (_transportClosed || _disposed)
{
return new ValueTask<bool>(false);
}
if (_activeWait)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentSendException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
}
_activeWait = true;
Debug.Assert(!_signaled);
_buffer = buffer;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpRawSendOperation)state)!.SetCanceled(), this);
_notification.Set(buffer.Length);
return new ValueTask<bool>(this, token);
}
public bool CancelPendingOperation(Exception innerException, CancellationToken cancellationToken)
{
lock (this)
{
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewOperationCanceledExceptionForCancelPendingSend(innerException, cancellationToken));
return true;
}
}
return false;
}
private void SetCanceled()
{
lock (this)
{
if (_activeWait && !_signaled)
{
CancellationToken cancellationToken = _cancellationToken;
ClearPreviousOperation();
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
private void ClearPreviousOperation()
{
_signaled = true;
_buffer = default;
_cancellationToken = default;
}
public bool TryConsume(Memory<byte> buffer, out int bytesWritten)
{
lock (this)
{
if (_transportClosed || _disposed)
{
bytesWritten = 0;
return false;
}
if (!_activeWait)
{
bytesWritten = 0;
return false;
}
ReadOnlyMemory<byte> source = _buffer;
if (source.Length > buffer.Length)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewMessageTooLargeForBufferArgument());
bytesWritten = 0;
return false;
}
source.CopyTo(buffer);
bytesWritten = source.Length;
ClearPreviousOperation();
_mrvtsc.SetResult(true);
return true;
}
}
public void SetTransportClosed()
{
lock (this)
{
if (_transportClosed || _disposed)
{
return;
}
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetResult(false);
}
_transportClosed = true;
}
}
public void Dispose()
{
lock (this)
{
if (_disposed)
{
return;
}
if (_activeWait && !_signaled)
{
ClearPreviousOperation();
_mrvtsc.SetResult(false);
}
_disposed = true;
_transportClosed = true;
}
}
}
}

View file

@ -0,0 +1,693 @@
using System.Threading.Tasks.Sources;
using System.Diagnostics;
#if NEED_LINKEDLIST_SHIM
using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#else
using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<(FurinaImpact.Kcp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(FurinaImpact.Kcp.KcpBuffer Data, byte Fragment)>;
#endif
namespace FurinaImpact.Kcp
{
internal sealed class KcpReceiveQueue : IValueTaskSource<KcpConversationReceiveResult>, IValueTaskSource<int>, IValueTaskSource<bool>, IDisposable
{
private ManualResetValueTaskSourceCore<KcpConversationReceiveResult> _mrvtsc;
private readonly LinkedListOfQueueItem _queue;
private readonly bool _stream;
private readonly int _queueSize;
private readonly KcpSendReceiveQueueItemCache _cache;
private int _completedPacketsCount;
private bool _transportClosed;
private bool _disposed;
private bool _activeWait;
private bool _signaled;
private byte _operationMode; // 0-receive 1-wait for message 2-wait for available data
private Memory<byte> _buffer;
private int _minimumBytes;
private int _minimumSegments;
private CancellationToken _cancellationToken;
private CancellationTokenRegistration _cancellationRegistration;
public KcpReceiveQueue(bool stream, int queueSize, KcpSendReceiveQueueItemCache cache)
{
_mrvtsc = new ManualResetValueTaskSourceCore<KcpConversationReceiveResult>()
{
RunContinuationsAsynchronously = true
};
_queue = new LinkedListOfQueueItem();
_stream = stream;
_queueSize = queueSize;
_cache = cache;
}
public ValueTaskSourceStatus GetStatus(short token) => _mrvtsc.GetStatus(token);
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
=> _mrvtsc.OnCompleted(continuation, state, token, flags);
KcpConversationReceiveResult IValueTaskSource<KcpConversationReceiveResult>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
int IValueTaskSource<int>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token).BytesReceived;
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
bool IValueTaskSource<bool>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return !_mrvtsc.GetResult(token).TransportClosed;
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signaled = false;
_cancellationRegistration = default;
}
}
}
public bool TryPeek(out KcpConversationReceiveResult result)
{
lock (_queue)
{
if (_disposed || _transportClosed)
{
result = default;
return false;
}
if (_activeWait)
{
ThrowHelper.ThrowConcurrentReceiveException();
}
if (_completedPacketsCount == 0)
{
result = new KcpConversationReceiveResult(0);
return false;
}
LinkedListNodeOfQueueItem node = _queue.First;
if (node is null)
{
result = new KcpConversationReceiveResult(0);
return false;
}
if (CalculatePacketSize(node, out int packetSize))
{
result = new KcpConversationReceiveResult(packetSize);
return true;
}
result = default;
return false;
}
}
public ValueTask<KcpConversationReceiveResult> WaitToReceiveAsync(CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return default;
}
if (_activeWait)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewConcurrentReceiveException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromCanceled<KcpConversationReceiveResult>(cancellationToken));
}
_operationMode = 1;
_buffer = default;
_minimumBytes = 0;
_minimumSegments = 0;
token = _mrvtsc.Version;
if (_completedPacketsCount > 0)
{
ConsumePacket(_buffer.Span, out KcpConversationReceiveResult result, out bool bufferTooSmall);
ClearPreviousOperation(false);
if (bufferTooSmall)
{
Debug.Assert(false, "This should never be reached.");
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewBufferTooSmallForBufferArgument()));
}
else
{
return new ValueTask<KcpConversationReceiveResult>(result);
}
}
_activeWait = true;
Debug.Assert(!_signaled);
_cancellationToken = cancellationToken;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue)state)!.SetCanceled(), this);
return new ValueTask<KcpConversationReceiveResult>(this, token);
}
public ValueTask<bool> WaitForAvailableDataAsync(int minimumBytes, int minimumSegments, CancellationToken cancellationToken)
{
if (minimumBytes < 0)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumBytes))));
}
if (minimumSegments < 0)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumSegments))));
}
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return default;
}
if (_activeWait)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentReceiveException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
}
if (CheckQueeuSize(_queue, minimumBytes, minimumSegments, _stream))
{
return new ValueTask<bool>(true);
}
_activeWait = true;
Debug.Assert(!_signaled);
_operationMode = 2;
_buffer = default;
_minimumBytes = minimumBytes;
_minimumSegments = minimumSegments;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue)state)!.SetCanceled(), this);
return new ValueTask<bool>(this, token);
}
public bool TryReceive(Span<byte> buffer, out KcpConversationReceiveResult result)
{
lock (_queue)
{
if (_disposed || _transportClosed)
{
result = default;
return false;
}
if (_activeWait)
{
ThrowHelper.ThrowConcurrentReceiveException();
}
if (_completedPacketsCount == 0)
{
result = new KcpConversationReceiveResult(0);
return false;
}
Debug.Assert(!_signaled);
_operationMode = 0;
ConsumePacket(buffer, out result, out bool bufferTooSmall);
ClearPreviousOperation(false);
if (bufferTooSmall)
{
ThrowHelper.ThrowBufferTooSmall();
}
return true;
}
}
public ValueTask<KcpConversationReceiveResult> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return default;
}
if (_activeWait)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewConcurrentReceiveException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromCanceled<KcpConversationReceiveResult>(cancellationToken));
}
_operationMode = 0;
_buffer = buffer;
token = _mrvtsc.Version;
if (_completedPacketsCount > 0)
{
ConsumePacket(_buffer.Span, out KcpConversationReceiveResult result, out bool bufferTooSmall);
ClearPreviousOperation(false);
if (bufferTooSmall)
{
return new ValueTask<KcpConversationReceiveResult>(Task.FromException<KcpConversationReceiveResult>(ThrowHelper.NewBufferTooSmallForBufferArgument()));
}
else
{
return new ValueTask<KcpConversationReceiveResult>(result);
}
}
_activeWait = true;
Debug.Assert(!_signaled);
_cancellationToken = cancellationToken;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue)state)!.SetCanceled(), this);
return new ValueTask<KcpConversationReceiveResult>(this, token);
}
public ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return new ValueTask<int>(Task.FromException<int>(ThrowHelper.NewTransportClosedForStreamException()));
}
if (_activeWait)
{
return new ValueTask<int>(Task.FromException<int>(ThrowHelper.NewConcurrentReceiveException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<int>(Task.FromCanceled<int>(cancellationToken));
}
_operationMode = 0;
_buffer = buffer;
token = _mrvtsc.Version;
if (_completedPacketsCount > 0)
{
ConsumePacket(_buffer.Span, out KcpConversationReceiveResult result, out bool bufferTooSmall);
ClearPreviousOperation(false);
if (bufferTooSmall)
{
return new ValueTask<int>(Task.FromException<int>(ThrowHelper.NewBufferTooSmallForBufferArgument()));
}
else
{
return new ValueTask<int>(result.BytesReceived);
}
}
_activeWait = true;
Debug.Assert(!_signaled);
_cancellationToken = cancellationToken;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue)state)!.SetCanceled(), this);
return new ValueTask<int>(this, token);
}
public bool CancelPendingOperation(Exception innerException, CancellationToken cancellationToken)
{
lock (_queue)
{
if (_activeWait && !_signaled)
{
ClearPreviousOperation(true);
_mrvtsc.SetException(ThrowHelper.NewOperationCanceledExceptionForCancelPendingReceive(innerException, cancellationToken));
return true;
}
}
return false;
}
private void SetCanceled()
{
lock (_queue)
{
if (_activeWait && !_signaled)
{
CancellationToken cancellationToken = _cancellationToken;
ClearPreviousOperation(true);
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
private void ClearPreviousOperation(bool signaled)
{
_signaled = signaled;
_operationMode = 0;
_buffer = default;
_minimumBytes = default;
_minimumSegments = default;
_cancellationToken = default;
}
public void Enqueue(KcpBuffer buffer, byte fragment)
{
lock (_queue)
{
if (_transportClosed || _disposed)
{
return;
}
if (_stream)
{
if (buffer.Length == 0)
{
return;
}
fragment = 0;
_queue.AddLast(_cache.Rent(buffer, 0));
}
else
{
LinkedListNodeOfQueueItem lastNode = _queue.Last;
if (lastNode is null || lastNode.ValueRef.Fragment == 0 || lastNode.ValueRef.Fragment - 1 == fragment)
{
_queue.AddLast(_cache.Rent(buffer, fragment));
}
else
{
fragment = 0;
_queue.AddLast(_cache.Rent(buffer, 0));
}
}
if (fragment == 0)
{
_completedPacketsCount++;
if (_activeWait && !_signaled)
{
TryCompleteReceive();
TryCompleteWaitForData();
}
}
}
}
private void TryCompleteReceive()
{
Debug.Assert(_activeWait && !_signaled);
if (_operationMode <= 1)
{
Debug.Assert(_operationMode == 0 || _operationMode == 1);
ConsumePacket(_buffer.Span, out KcpConversationReceiveResult result, out bool bufferTooSmall);
ClearPreviousOperation(true);
if (bufferTooSmall)
{
_mrvtsc.SetException(ThrowHelper.NewBufferTooSmallForBufferArgument());
}
else
{
_mrvtsc.SetResult(result);
}
}
}
private void TryCompleteWaitForData()
{
if (_operationMode == 2)
{
if (CheckQueeuSize(_queue, _minimumBytes, _minimumSegments, _stream))
{
ClearPreviousOperation(true);
_mrvtsc.SetResult(new KcpConversationReceiveResult(0));
}
}
}
private void ConsumePacket(Span<byte> buffer, out KcpConversationReceiveResult result, out bool bufferTooSmall)
{
LinkedListNodeOfQueueItem node = _queue.First;
if (node is null)
{
result = default;
bufferTooSmall = false;
return;
}
// peek
if (_operationMode == 1)
{
if (CalculatePacketSize(node, out int bytesRecevied))
{
result = new KcpConversationReceiveResult(bytesRecevied);
}
else
{
result = default;
}
bufferTooSmall = false;
return;
}
Debug.Assert(_operationMode == 0);
// ensure buffer is big enough
int bytesInPacket = 0;
if (!_stream)
{
while (node is not null)
{
bytesInPacket += node.ValueRef.Data.Length;
if (node.ValueRef.Fragment == 0)
{
break;
}
node = node.Next;
}
if (node is null)
{
// incomplete packet
result = default;
bufferTooSmall = false;
return;
}
if (bytesInPacket > buffer.Length)
{
result = default;
bufferTooSmall = true;
return;
}
}
bool anyDataReceived = false;
bytesInPacket = 0;
node = _queue.First;
LinkedListNodeOfQueueItem next;
while (node is not null)
{
next = node.Next;
byte fragment = node.ValueRef.Fragment;
ref KcpBuffer data = ref node.ValueRef.Data;
int sizeToCopy = Math.Min(data.Length, buffer.Length);
data.DataRegion.Span.Slice(0, sizeToCopy).CopyTo(buffer);
buffer = buffer.Slice(sizeToCopy);
bytesInPacket += sizeToCopy;
anyDataReceived = true;
if (sizeToCopy != data.Length)
{
// partial data is received.
node.ValueRef = (data.Consume(sizeToCopy), node.ValueRef.Fragment);
}
else
{
// full fragment is consumed
data.Release();
_queue.Remove(node);
_cache.Return(node);
if (fragment == 0)
{
_completedPacketsCount--;
}
}
if (!_stream && fragment == 0)
{
break;
}
if (sizeToCopy == 0)
{
break;
}
node = next;
}
if (!anyDataReceived)
{
result = default;
bufferTooSmall = false;
}
else
{
result = new KcpConversationReceiveResult(bytesInPacket);
bufferTooSmall = false;
}
}
private static bool CalculatePacketSize(LinkedListNodeOfQueueItem first, out int packetSize)
{
int bytesRecevied = first.ValueRef.Data.Length;
if (first.ValueRef.Fragment == 0)
{
packetSize = bytesRecevied;
return true;
}
LinkedListNodeOfQueueItem node = first.Next;
while (node is not null)
{
bytesRecevied += node.ValueRef.Data.Length;
if (node.ValueRef.Fragment == 0)
{
packetSize = bytesRecevied;
return true;
}
node = node.Next;
}
// deadlink
packetSize = 0;
return false;
}
private static bool CheckQueeuSize(LinkedListOfQueueItem queue, int minimumBytes, int minimumSegments, bool stream)
{
LinkedListNodeOfQueueItem node = queue.First;
while (node is not null)
{
ref KcpBuffer buffer = ref node.ValueRef.Data;
minimumBytes = Math.Max(minimumBytes - buffer.Length, 0);
if (stream || node.ValueRef.Fragment == 0)
{
minimumSegments = Math.Max(minimumSegments - 1, 0);
}
if (minimumBytes == 0 && minimumSegments == 0)
{
return true;
}
node = node.Next;
}
return minimumBytes == 0 && minimumSegments == 0;
}
public void SetTransportClosed()
{
lock (_queue)
{
if (_transportClosed || _disposed)
{
return;
}
if (_activeWait && !_signaled)
{
ClearPreviousOperation(true);
_mrvtsc.SetResult(default);
}
_transportClosed = true;
}
}
public int GetQueueSize()
{
int count;
lock (_queue)
{
count = _queue.Count;
}
return Math.Max(_queue.Count - _queueSize, 0);
}
public void Dispose()
{
lock (_queue)
{
if (_disposed)
{
return;
}
if (_activeWait && !_signaled)
{
ClearPreviousOperation(true);
_mrvtsc.SetResult(default);
}
LinkedListNodeOfQueueItem node = _queue.First;
while (node is not null)
{
node.ValueRef.Data.Release();
node = node.Next;
}
_queue.Clear();
_disposed = true;
_transportClosed = true;
}
}
}
}

View file

@ -0,0 +1,37 @@
namespace FurinaImpact.Kcp
{
/// <summary>
/// Options for sending receive window size notification.
/// </summary>
public sealed class KcpReceiveWindowNotificationOptions
{
/// <summary>
/// Create an instance of option object for receive window size notification functionality.
/// </summary>
/// <param name="initialInterval">The initial interval in milliseconds of sending window size notification.</param>
/// <param name="maximumInterval">The maximum interval in milliseconds of sending window size notification.</param>
public KcpReceiveWindowNotificationOptions(int initialInterval, int maximumInterval)
{
if (initialInterval <= 0)
{
throw new ArgumentOutOfRangeException(nameof(initialInterval));
}
if (maximumInterval < initialInterval)
{
throw new ArgumentOutOfRangeException(nameof(maximumInterval));
}
InitialInterval = initialInterval;
MaximumInterval = maximumInterval;
}
/// <summary>
/// The initial interval in milliseconds of sending window size notification.
/// </summary>
public int InitialInterval { get; }
/// <summary>
/// The maximum interval in milliseconds of sending window size notification.
/// </summary>
public int MaximumInterval { get; }
}
}

View file

@ -0,0 +1,222 @@
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace FurinaImpact.Kcp
{
/// <summary>
/// The buffer rented and owned by KcpSharp.
/// </summary>
public readonly struct KcpRentedBuffer : IEquatable<KcpRentedBuffer>, IDisposable
{
private readonly object _owner;
private readonly Memory<byte> _memory;
internal object Owner => _owner;
/// <summary>
/// The rented buffer.
/// </summary>
public Memory<byte> Memory => _memory;
/// <summary>
/// The rented buffer.
/// </summary>
public Span<byte> Span => _memory.Span;
/// <summary>
/// Whether this struct contains buffer rented from the pool.
/// </summary>
public bool IsAllocated => _owner is not null;
/// <summary>
/// Whether this buffer contains no data.
/// </summary>
public bool IsEmpry => _memory.IsEmpty;
internal KcpRentedBuffer(object owner, Memory<byte> buffer)
{
_owner = owner;
_memory = buffer;
}
/// <summary>
/// Create the buffer from the specified <see cref="Memory{T}"/>.
/// </summary>
/// <param name="memory">The memory region of this buffer.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromMemory(Memory<byte> memory)
{
return new KcpRentedBuffer(null, memory);
}
/// <summary>
/// Create the buffer from the shared array pool.
/// </summary>
/// <param name="size">The minimum size of the buffer required.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromSharedArrayPool(int size)
{
if (size < 0)
{
throw new ArgumentOutOfRangeException(nameof(size));
}
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
return new KcpRentedBuffer(ArrayPool<byte>.Shared, buffer);
}
/// <summary>
/// Create the buffer from the specified array pool.
/// </summary>
/// <param name="pool">The array pool to use.</param>
/// <param name="buffer">The byte array rented from the specified pool.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromArrayPool(ArrayPool<byte> pool, byte[] buffer)
{
if (pool is null)
{
throw new ArgumentNullException(nameof(pool));
}
if (buffer is null)
{
throw new ArgumentNullException(nameof(buffer));
}
return new KcpRentedBuffer(pool, buffer);
}
/// <summary>
/// Create the buffer from the specified array pool.
/// </summary>
/// <param name="pool">The array pool to use.</param>
/// <param name="arraySegment">The byte array segment rented from the specified pool.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromArrayPool(ArrayPool<byte> pool, ArraySegment<byte> arraySegment)
{
if (pool is null)
{
throw new ArgumentNullException(nameof(pool));
}
return new KcpRentedBuffer(pool, arraySegment);
}
/// <summary>
/// Create the buffer from the specified array pool.
/// </summary>
/// <param name="pool">The array pool to use.</param>
/// <param name="size">The minimum size of the buffer required.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromArrayPool(ArrayPool<byte> pool, int size)
{
if (pool is null)
{
throw new ArgumentNullException(nameof(pool));
}
if (size < 0)
{
throw new ArgumentOutOfRangeException(nameof(size));
}
return new KcpRentedBuffer(pool, pool.Rent(size));
}
/// <summary>
/// Create the buffer from the memory owner.
/// </summary>
/// <param name="memoryOwner">The owner of this memory region.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromMemoryOwner(IMemoryOwner<byte> memoryOwner)
{
if (memoryOwner is null)
{
throw new ArgumentNullException(nameof(memoryOwner));
}
return new KcpRentedBuffer(memoryOwner, memoryOwner.Memory);
}
/// <summary>
/// Create the buffer from the memory owner.
/// </summary>
/// <param name="memoryOwner">The owner of this memory region.</param>
/// <param name="memory">The memory region of the buffer.</param>
/// <returns>The rented buffer.</returns>
public static KcpRentedBuffer FromMemoryOwner(IDisposable memoryOwner, Memory<byte> memory)
{
if (memoryOwner is null)
{
throw new ArgumentNullException(nameof(memoryOwner));
}
return new KcpRentedBuffer(memoryOwner, memory);
}
/// <summary>
/// Forms a slice out of the current buffer that begins at a specified index.
/// </summary>
/// <param name="start">The index at which to begin the slice.</param>
/// <returns>An object that contains all elements of the current instance from start to the end of the instance.</returns>
public KcpRentedBuffer Slice(int start)
{
Memory<byte> memory = _memory;
if ((uint)start > (uint)memory.Length)
{
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(start));
}
return new KcpRentedBuffer(_owner, memory.Slice(start));
}
/// <summary>
/// Forms a slice out of the current memory starting at a specified index for a specified length.
/// </summary>
/// <param name="start">The index at which to begin the slice.</param>
/// <param name="length">The number of elements to include in the slice.</param>
/// <returns>An object that contains <paramref name="length"/> elements from the current instance starting at <paramref name="start"/>.</returns>
public KcpRentedBuffer Slice(int start, int length)
{
Memory<byte> memory = _memory;
if ((uint)start > (uint)memory.Length)
{
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(start));
}
if ((uint)length > (uint)(memory.Length - start))
{
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(length));
}
return new KcpRentedBuffer(_owner, memory.Slice(start, length));
}
/// <inheritdoc />
public void Dispose()
{
Debug.Assert(_owner is null || _owner is ArrayPool<byte> || _owner is IDisposable);
if (_owner is null)
{
return;
}
if (_owner is ArrayPool<byte> arrayPool)
{
if (MemoryMarshal.TryGetArray(_memory, out ArraySegment<byte> arraySegment))
{
arrayPool.Return(arraySegment.Array!);
return;
}
}
if (_owner is IDisposable disposable)
{
disposable.Dispose();
}
}
/// <inheritdoc />
public bool Equals(KcpRentedBuffer other) => ReferenceEquals(_owner, other._owner) && _memory.Equals(other._memory);
/// <inheritdoc />
public override bool Equals(object obj) => obj is KcpRentedBuffer other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => _owner is null ? _memory.GetHashCode() : HashCode.Combine(RuntimeHelpers.GetHashCode(_owner), _memory);
/// <inheritdoc />
public override string ToString() => $"KcpSharp.KcpRentedBuffer[{_memory.Length}]";
}
}

View file

@ -0,0 +1,715 @@
using System.Diagnostics;
using System.Threading.Tasks.Sources;
#if NEED_LINKEDLIST_SHIM
using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#else
using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<(FurinaImpact.Kcp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(FurinaImpact.Kcp.KcpBuffer Data, byte Fragment)>;
#endif
namespace FurinaImpact.Kcp
{
internal sealed class KcpSendQueue : IValueTaskSource<bool>, IValueTaskSource, IDisposable
{
private readonly IKcpBufferPool _bufferPool;
private readonly KcpConversationUpdateActivation _updateActivation;
private readonly bool _stream;
private readonly int _capacity;
private readonly int _mss;
private readonly KcpSendReceiveQueueItemCache _cache;
private ManualResetValueTaskSourceCore<bool> _mrvtsc;
private readonly LinkedListOfQueueItem _queue;
private long _unflushedBytes;
private bool _transportClosed;
private bool _disposed;
private bool _activeWait;
private bool _signled;
private bool _forStream;
private byte _operationMode; // 0-send 1-flush 2-wait for space
private ReadOnlyMemory<byte> _buffer;
private int _waitForByteCount;
private int _waitForSegmentCount;
private CancellationToken _cancellationToken;
private CancellationTokenRegistration _cancellationRegistration;
private bool _ackListNotEmpty;
public KcpSendQueue(IKcpBufferPool bufferPool, KcpConversationUpdateActivation updateActivation, bool stream, int capacity, int mss, KcpSendReceiveQueueItemCache cache)
{
_bufferPool = bufferPool;
_updateActivation = updateActivation;
_stream = stream;
_capacity = capacity;
_mss = mss;
_cache = cache;
_mrvtsc = new ManualResetValueTaskSourceCore<bool>()
{
RunContinuationsAsynchronously = true
};
_queue = new LinkedListOfQueueItem();
}
public ValueTaskSourceStatus GetStatus(short token) => _mrvtsc.GetStatus(token);
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
=> _mrvtsc.OnCompleted(continuation, state, token, flags);
bool IValueTaskSource<bool>.GetResult(short token)
{
_cancellationRegistration.Dispose();
try
{
return _mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signled = false;
_cancellationRegistration = default;
}
}
}
void IValueTaskSource.GetResult(short token)
{
try
{
_mrvtsc.GetResult(token);
}
finally
{
_mrvtsc.Reset();
lock (_queue)
{
_activeWait = false;
_signled = false;
_cancellationRegistration = default;
}
}
}
public bool TryGetAvailableSpace(out int byteCount, out int segmentCount)
{
lock (_queue)
{
if (_transportClosed || _disposed)
{
byteCount = 0;
segmentCount = 0;
return false;
}
if (_activeWait && _operationMode == 0)
{
byteCount = 0;
segmentCount = 0;
return true;
}
GetAvailableSpaceCore(out byteCount, out segmentCount);
return true;
}
}
private void GetAvailableSpaceCore(out int byteCount, out int segmentCount)
{
int mss = _mss;
int availableFragments = _capacity - _queue.Count;
if (availableFragments < 0)
{
byteCount = 0;
segmentCount = 0;
return;
}
int availableBytes = availableFragments * mss;
if (_stream)
{
LinkedListNodeOfQueueItem last = _queue.Last;
if (last is not null)
{
availableBytes += _mss - last.ValueRef.Data.Length;
}
}
byteCount = availableBytes;
segmentCount = availableFragments;
}
public ValueTask<bool> WaitForAvailableSpaceAsync(int minimumBytes, int minimumSegments, CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
minimumBytes = 0;
minimumSegments = 0;
return default;
}
if ((uint)minimumBytes > (uint)(_mss * _capacity))
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumBytes))));
}
if ((uint)minimumSegments > (uint)_capacity)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumSegments))));
}
if (_activeWait)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentSendException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
}
GetAvailableSpaceCore(out int currentByteCount, out int currentSegmentCount);
if (currentByteCount >= minimumBytes && currentSegmentCount >= minimumSegments)
{
return new ValueTask<bool>(true);
}
_activeWait = true;
Debug.Assert(!_signled);
_forStream = false;
_operationMode = 2;
_waitForByteCount = minimumBytes;
_waitForSegmentCount = minimumSegments;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue)state)!.SetCanceled(), this);
return new ValueTask<bool>(this, token);
}
public bool TrySend(ReadOnlySpan<byte> buffer, bool allowPartialSend, out int bytesWritten)
{
lock (_queue)
{
if (allowPartialSend && !_stream)
{
ThrowHelper.ThrowAllowPartialSendArgumentException();
}
if (_transportClosed || _disposed)
{
bytesWritten = 0;
return false;
}
int mss = _mss;
// Make sure there is enough space.
if (!allowPartialSend)
{
int spaceAvailable = mss * (_capacity - _queue.Count);
if (spaceAvailable < 0)
{
bytesWritten = 0;
return false;
}
if (_stream)
{
LinkedListNodeOfQueueItem last = _queue.Last;
if (last is not null)
{
spaceAvailable += mss - last.ValueRef.Data.Length;
}
}
if (buffer.Length > spaceAvailable)
{
bytesWritten = 0;
return false;
}
}
// Copy buffer content.
bytesWritten = 0;
if (_stream)
{
LinkedListNodeOfQueueItem node = _queue.Last;
if (node is not null)
{
ref KcpBuffer data = ref node.ValueRef.Data;
int expand = mss - data.Length;
expand = Math.Min(expand, buffer.Length);
if (expand > 0)
{
data = data.AppendData(buffer.Slice(0, expand));
buffer = buffer.Slice(expand);
Interlocked.Add(ref _unflushedBytes, expand);
bytesWritten = expand;
}
}
if (buffer.IsEmpty)
{
return true;
}
}
bool anySegmentAdded = false;
int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
Debug.Assert(count >= 1);
while (count > 0 && _queue.Count < _capacity)
{
int fragment = --count;
int size = buffer.Length > mss ? mss : buffer.Length;
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
KcpBuffer kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Slice(0, size));
buffer = buffer.Slice(size);
_queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)fragment));
Interlocked.Add(ref _unflushedBytes, size);
bytesWritten += size;
anySegmentAdded = true;
}
if (anySegmentAdded)
{
_updateActivation.Notify();
}
return anySegmentAdded;
}
}
public ValueTask<bool> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return new ValueTask<bool>(false);
}
if (_activeWait)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentSendException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
}
int mss = _mss;
if (_stream)
{
LinkedListNodeOfQueueItem node = _queue.Last;
if (node is not null)
{
ref KcpBuffer data = ref node.ValueRef.Data;
int expand = mss - data.Length;
expand = Math.Min(expand, buffer.Length);
if (expand > 0)
{
data = data.AppendData(buffer.Span.Slice(0, expand));
buffer = buffer.Slice(expand);
Interlocked.Add(ref _unflushedBytes, expand);
}
}
if (buffer.IsEmpty)
{
return new ValueTask<bool>(true);
}
}
int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
Debug.Assert(count >= 1);
if (!_stream && count > 256)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewMessageTooLargeForBufferArgument()));
}
// synchronously put fragments into queue.
while (count > 0 && _queue.Count < _capacity)
{
int fragment = --count;
int size = buffer.Length > mss ? mss : buffer.Length;
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
KcpBuffer kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size));
buffer = buffer.Slice(size);
_queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)fragment));
Interlocked.Add(ref _unflushedBytes, size);
}
_updateActivation.Notify();
if (count == 0)
{
return new ValueTask<bool>(true);
}
_activeWait = true;
Debug.Assert(!_signled);
_forStream = false;
_operationMode = 0;
_buffer = buffer;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue)state)!.SetCanceled(), this);
return new ValueTask<bool>(this, token);
}
public ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return new ValueTask(Task.FromException(ThrowHelper.NewTransportClosedForStreamException()));
}
if (_activeWait)
{
return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask(Task.FromCanceled(cancellationToken));
}
int mss = _mss;
if (_stream)
{
LinkedListNodeOfQueueItem node = _queue.Last;
if (node is not null)
{
ref KcpBuffer data = ref node.ValueRef.Data;
int expand = mss - data.Length;
expand = Math.Min(expand, buffer.Length);
if (expand > 0)
{
data = data.AppendData(buffer.Span.Slice(0, expand));
buffer = buffer.Slice(expand);
Interlocked.Add(ref _unflushedBytes, expand);
}
}
if (buffer.IsEmpty)
{
return default;
}
}
int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
Debug.Assert(count >= 1);
Debug.Assert(_stream);
// synchronously put fragments into queue.
while (count > 0 && _queue.Count < _capacity)
{
int size = buffer.Length > mss ? mss : buffer.Length;
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
KcpBuffer kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size));
buffer = buffer.Slice(size);
_queue.AddLast(_cache.Rent(kcpBuffer, 0));
Interlocked.Add(ref _unflushedBytes, size);
}
_updateActivation.Notify();
if (count == 0)
{
return default;
}
_activeWait = true;
Debug.Assert(!_signled);
_forStream = true;
_operationMode = 0;
_buffer = buffer;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue)state)!.SetCanceled(), this);
return new ValueTask(this, token);
}
public ValueTask<bool> FlushAsync(CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return new ValueTask<bool>(false);
}
if (_activeWait)
{
return new ValueTask<bool>(Task.FromException<bool>(ThrowHelper.NewConcurrentSendException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<bool>(Task.FromCanceled<bool>(cancellationToken));
}
_activeWait = true;
Debug.Assert(!_signled);
_forStream = false;
_operationMode = 1;
_buffer = default;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue)state)!.SetCanceled(), this);
return new ValueTask<bool>(this, token);
}
public ValueTask FlushForStreamAsync(CancellationToken cancellationToken)
{
short token;
lock (_queue)
{
if (_transportClosed || _disposed)
{
return new ValueTask(Task.FromException(ThrowHelper.NewTransportClosedForStreamException()));
}
if (_activeWait)
{
return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException()));
}
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask(Task.FromCanceled(cancellationToken));
}
_activeWait = true;
Debug.Assert(!_signled);
_forStream = true;
_operationMode = 1;
_buffer = default;
_cancellationToken = cancellationToken;
token = _mrvtsc.Version;
}
_cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue)state)!.SetCanceled(), this);
return new ValueTask(this, token);
}
public bool CancelPendingOperation(Exception innerException, CancellationToken cancellationToken)
{
lock (_queue)
{
if (_activeWait && !_signled)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewOperationCanceledExceptionForCancelPendingSend(innerException, cancellationToken));
return true;
}
}
return false;
}
private void SetCanceled()
{
lock (_queue)
{
if (_activeWait && !_signled)
{
CancellationToken cancellationToken = _cancellationToken;
ClearPreviousOperation();
_mrvtsc.SetException(new OperationCanceledException(cancellationToken));
}
}
}
private void ClearPreviousOperation()
{
_signled = true;
_forStream = false;
_operationMode = 0;
_buffer = default;
_waitForByteCount = default;
_waitForSegmentCount = default;
_cancellationToken = default;
}
public bool TryDequeue(out KcpBuffer data, out byte fragment)
{
lock (_queue)
{
LinkedListNodeOfQueueItem node = _queue.First;
if (node is null)
{
data = default;
fragment = default;
return false;
}
else
{
(data, fragment) = node.ValueRef;
_queue.RemoveFirst();
node.ValueRef = default;
_cache.Return(node);
MoveOneSegmentIn();
CheckForAvailableSpace();
return true;
}
}
}
public void NotifyAckListChanged(bool itemsListNotEmpty)
{
lock (_queue)
{
if (_transportClosed || _disposed)
{
return;
}
_ackListNotEmpty = itemsListNotEmpty;
TryCompleteFlush(Interlocked.Read(ref _unflushedBytes));
}
}
private void MoveOneSegmentIn()
{
if (_activeWait && !_signled && _operationMode == 0)
{
ReadOnlyMemory<byte> buffer = _buffer;
int mss = _mss;
int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss;
int size = buffer.Length > mss ? mss : buffer.Length;
KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false));
KcpBuffer kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size));
_buffer = buffer.Slice(size);
_queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)(count - 1)));
Interlocked.Add(ref _unflushedBytes, size);
if (count == 1)
{
ClearPreviousOperation();
_mrvtsc.SetResult(true);
}
}
}
private void CheckForAvailableSpace()
{
if (_activeWait && !_signled && _operationMode == 2)
{
GetAvailableSpaceCore(out int byteCount, out int segmentCount);
if (byteCount >= _waitForByteCount && segmentCount >= _waitForSegmentCount)
{
ClearPreviousOperation();
_mrvtsc.SetResult(true);
}
}
}
private void TryCompleteFlush(long unflushedBytes)
{
if (_activeWait && !_signled && _operationMode == 1)
{
if (_queue.Last is null && unflushedBytes == 0 && !_ackListNotEmpty)
{
ClearPreviousOperation();
_mrvtsc.SetResult(true);
}
}
}
public void SubtractUnflushedBytes(int size)
{
long unflushedBytes = Interlocked.Add(ref _unflushedBytes, -size);
if (unflushedBytes == 0)
{
lock (_queue)
{
TryCompleteFlush(0);
}
}
}
public long GetUnflushedBytes()
{
if (_transportClosed || _disposed)
{
return 0;
}
return Interlocked.Read(ref _unflushedBytes);
}
public void SetTransportClosed()
{
lock (_queue)
{
if (_transportClosed || _disposed)
{
return;
}
if (_activeWait && !_signled)
{
if (_forStream)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewTransportClosedForStreamException());
}
else
{
ClearPreviousOperation();
_mrvtsc.SetResult(false);
}
}
_transportClosed = true;
Interlocked.Exchange(ref _unflushedBytes, 0);
}
}
public void Dispose()
{
lock (_queue)
{
if (_disposed)
{
return;
}
if (_activeWait && !_signled)
{
if (_forStream)
{
ClearPreviousOperation();
_mrvtsc.SetException(ThrowHelper.NewTransportClosedForStreamException());
}
else
{
ClearPreviousOperation();
_mrvtsc.SetResult(false);
}
}
LinkedListNodeOfQueueItem node = _queue.First;
while (node is not null)
{
node.ValueRef.Data.Release();
node = node.Next;
}
_queue.Clear();
_disposed = true;
_transportClosed = true;
}
}
}
}

View file

@ -0,0 +1,9 @@
namespace FurinaImpact.Kcp
{
internal struct KcpSendReceiveBufferItem
{
public KcpBuffer Data;
public KcpPacketHeader Segment;
public KcpSendSegmentStats Stats;
}
}

View file

@ -0,0 +1,73 @@

#if NEED_LINKEDLIST_SHIM
using LinkedListOfBufferItem = KcpSharp.NetstandardShim.LinkedList<KcpSharp.KcpSendReceiveBufferItem>;
using LinkedListNodeOfBufferItem = KcpSharp.NetstandardShim.LinkedListNode<KcpSharp.KcpSendReceiveBufferItem>;
#else
using LinkedListNodeOfBufferItem = System.Collections.Generic.LinkedListNode<FurinaImpact.Kcp.KcpSendReceiveBufferItem>;
using LinkedListOfBufferItem = System.Collections.Generic.LinkedList<FurinaImpact.Kcp.KcpSendReceiveBufferItem>;
#endif
namespace FurinaImpact.Kcp
{
internal struct KcpSendReceiveBufferItemCache
{
private LinkedListOfBufferItem _items;
private SpinLock _lock;
public static KcpSendReceiveBufferItemCache Create()
{
return new KcpSendReceiveBufferItemCache
{
_items = new LinkedListOfBufferItem(),
_lock = new SpinLock()
};
}
public LinkedListNodeOfBufferItem Allocate(in KcpSendReceiveBufferItem item)
{
bool lockAcquired = false;
try
{
_lock.Enter(ref lockAcquired);
LinkedListNodeOfBufferItem node = _items.First;
if (node is null)
{
node = new LinkedListNodeOfBufferItem(item);
}
else
{
_items.Remove(node);
node.ValueRef = item;
}
return node;
}
finally
{
if (lockAcquired)
{
_lock.Exit();
}
}
}
public void Return(LinkedListNodeOfBufferItem node)
{
bool lockAcquired = false;
try
{
_lock.Enter(ref lockAcquired);
node.ValueRef = default;
_items.AddLast(node);
}
finally
{
if (lockAcquired)
{
_lock.Exit();
}
}
}
}
}

View file

@ -0,0 +1,84 @@

#if NEED_LINKEDLIST_SHIM
using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>;
using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>;
#else
using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(FurinaImpact.Kcp.KcpBuffer Data, byte Fragment)>;
using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<(FurinaImpact.Kcp.KcpBuffer Data, byte Fragment)>;
#endif
namespace FurinaImpact.Kcp
{
internal sealed class KcpSendReceiveQueueItemCache
{
private LinkedListOfQueueItem _list = new();
private SpinLock _lock;
public LinkedListNodeOfQueueItem Rent(in KcpBuffer buffer, byte fragment)
{
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
LinkedListNodeOfQueueItem node = _list.First;
if (node is null)
{
node = new LinkedListNodeOfQueueItem((buffer, fragment));
}
else
{
node.ValueRef = (buffer, fragment);
_list.RemoveFirst();
}
return node;
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
}
public void Return(LinkedListNodeOfQueueItem node)
{
node.ValueRef = default;
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_list.AddLast(node);
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
}
public void Clear()
{
bool lockTaken = false;
try
{
_lock.Enter(ref lockTaken);
_list.Clear();
}
finally
{
if (lockTaken)
{
_lock.Exit();
}
}
}
}
}

View file

@ -0,0 +1,20 @@
namespace FurinaImpact.Kcp
{
internal readonly struct KcpSendSegmentStats
{
public KcpSendSegmentStats(uint resendTimestamp, uint rto, uint fastAck, uint transmitCount)
{
ResendTimestamp = resendTimestamp;
Rto = rto;
FastAck = fastAck;
TransmitCount = transmitCount;
}
public uint ResendTimestamp { get; }
public uint Rto { get; }
public uint FastAck { get; }
public uint TransmitCount { get; }
}
}

View file

@ -0,0 +1,158 @@
using System.Net;
using System.Net.Sockets;
namespace FurinaImpact.Kcp
{
/// <summary>
/// Helper methods to create socket transports for KCP conversations.
/// </summary>
public static class KcpSocketTransport
{
/// <summary>
/// Create a socket transport for KCP covnersation.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="options">The options of the <see cref="KcpConversation"/>.</param>
/// <returns>The created socket transport instance.</returns>
public static IKcpTransport<KcpConversation> CreateConversation(UdpClient listener, IPEndPoint endPoint, long conversationId, KcpConversationOptions options)
{
if (listener is null)
{
throw new ArgumentNullException(nameof(listener));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForConversation(listener, endPoint, conversationId, options);
}
/// <summary>
/// Create a socket transport for KCP covnersation with no conversation ID.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="options">The options of the <see cref="KcpConversation"/>.</param>
/// <returns>The created socket transport instance.</returns>
public static IKcpTransport<KcpConversation> CreateConversation(UdpClient listener, IPEndPoint endPoint, KcpConversationOptions options)
{
if (listener is null)
{
throw new ArgumentNullException(nameof(listener));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForConversation(listener, endPoint, null, options);
}
/// <summary>
/// Create a socket transport for raw channel.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="conversationId">The conversation ID.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel"/>.</param>
/// <returns>The created socket transport instance.</returns>
public static IKcpTransport<KcpRawChannel> CreateRawChannel(UdpClient listener, IPEndPoint endPoint, long conversationId, KcpRawChannelOptions options)
{
if (listener is null)
{
throw new ArgumentNullException(nameof(listener));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForRawChannel(listener, endPoint, conversationId, options);
}
/// <summary>
/// Create a socket transport for raw channel with no conversation ID.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="options">The options of the <see cref="KcpRawChannel"/>.</param>
/// <returns>The created socket transport instance.</returns>
public static IKcpTransport<KcpRawChannel> CreateRawChannel(UdpClient listener, IPEndPoint endPoint, KcpRawChannelOptions options)
{
if (listener is null)
{
throw new ArgumentNullException(nameof(listener));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForRawChannel(listener, endPoint, null, options);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <param name="listener">The udp listener instance.</param>
/// <param name="mtu">The maximum packet size that can be transmitted over the socket.</param>
/// <returns></returns>
public static IKcpTransport<IKcpMultiplexConnection> CreateMultiplexConnection(UdpClient listener, int mtu)
{
if (listener is null)
{
throw new ArgumentNullException(nameof(listener));
}
return new KcpSocketTransportForMultiplexConnection<object>(listener, mtu);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <typeparam name="T">The type of the user state.</typeparam>
/// <param name="listener">The udp listener instance.</param>
/// <param name="mtu">The maximum packet size that can be transmitted over the socket.</param>
/// <returns></returns>
public static IKcpTransport<IKcpMultiplexConnection<T>> CreateMultiplexConnection<T>(UdpClient listener, IPEndPoint endPoint, int mtu)
{
if (listener is null)
{
throw new ArgumentNullException(nameof(listener));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForMultiplexConnection<T>(listener, mtu);
}
/// <summary>
/// Create a socket transport for multiplex connection.
/// </summary>
/// <typeparam name="T">The type of the user state.</typeparam>
/// <param name="listener">The udp listener instance.</param>
/// <param name="endPoint">The remote endpoint.</param>
/// <param name="mtu">The maximum packet size that can be transmitted over the socket.</param>
/// <param name="disposeAction">The action to invoke when state object is removed.</param>
/// <returns></returns>
public static IKcpTransport<IKcpMultiplexConnection<T>> CreateMultiplexConnection<T>(UdpClient listener, EndPoint endPoint, int mtu, Action<T?> disposeAction)
{
if (listener is null)
{
throw new ArgumentNullException(nameof(listener));
}
if (endPoint is null)
{
throw new ArgumentNullException(nameof(endPoint));
}
return new KcpSocketTransportForMultiplexConnection<T>(listener, mtu, disposeAction);
}
}
}

View file

@ -0,0 +1,45 @@
using System.Net;
using System.Net.Sockets;
namespace FurinaImpact.Kcp
{
/// <summary>
/// Socket transport for KCP conversation.
/// </summary>
internal sealed class KcpSocketTransportForConversation : KcpSocketTransport<KcpConversation>, IKcpTransport<KcpConversation>
{
private readonly long? _conversationId;
private readonly IPEndPoint _remoteEndPoint;
private readonly KcpConversationOptions _options;
private Func<Exception, IKcpTransport<KcpConversation>, object, bool> _exceptionHandler;
private object _exceptionHandlerState;
internal KcpSocketTransportForConversation(UdpClient listener, IPEndPoint endPoint, long? conversationId, KcpConversationOptions options)
: base(listener, options?.Mtu ?? KcpConversationOptions.MtuDefaultValue)
{
_conversationId = conversationId;
_remoteEndPoint = endPoint;
_options = options;
}
protected override KcpConversation Activate() => _conversationId.HasValue ? new KcpConversation(_remoteEndPoint, this, _conversationId.GetValueOrDefault(), _options) : new KcpConversation(_remoteEndPoint, this, _options);
protected override bool HandleException(Exception ex)
{
if (_exceptionHandler is not null)
{
return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState);
}
return false;
}
public void SetExceptionHandler(Func<Exception, IKcpTransport<KcpConversation>, object, bool> handler, object state)
{
_exceptionHandler = handler;
_exceptionHandlerState = state;
}
}
}

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