1- using Dapper ;
2-
3- using Darkages . Enums ;
4- using Darkages . Sprites ;
5- using Microsoft . Data . SqlClient ;
1+ using System . Collections . Concurrent ;
62using System . Data ;
73using System . Numerics ;
4+
5+ using Dapper ;
6+
7+ using Darkages . Common ;
8+ using Darkages . Enums ;
89using Darkages . Models ;
10+ using Darkages . Network . Server ;
11+ using Darkages . Sprites ;
912using Darkages . Templates ;
13+
14+ using Microsoft . Data . SqlClient ;
1015using Microsoft . Extensions . Logging ;
11- using Darkages . Network . Server ;
12- using Darkages . Common ;
1316
1417namespace Darkages . Database ;
1518
@@ -18,9 +21,11 @@ public record AislingStorage : Sql, IEqualityOperators<AislingStorage, AislingSt
1821 public const string ConnectionString = "Data Source=.;Initial Catalog=ZolianPlayers;Integrated Security=True;Encrypt=False;MultipleActiveResultSets=True;" ;
1922 public const string PersonalMailString = "Data Source=.;Initial Catalog=ZolianBoardsMail;Integrated Security=True;Encrypt=False;MultipleActiveResultSets=True;" ;
2023 private const string EncryptedConnectionString = "Data Source=.;Initial Catalog=ZolianPlayers;Integrated Security=True;Column Encryption Setting=enabled;TrustServerCertificate=True;MultipleActiveResultSets=True;" ;
21- public AsyncLock SaveLock { get ; } = new ( ) ;
2224 private AsyncLock LoadLock { get ; } = new ( ) ;
2325 private AsyncLock CreateLock { get ; } = new ( ) ;
26+ private readonly ConcurrentDictionary < uint , AsyncLock > _playerLocks = new ( ) ;
27+ public AsyncLock GetPlayerLock ( uint serial ) => _playerLocks . GetOrAdd ( serial , static _ => new AsyncLock ( ) ) ;
28+ public void TryRemovePlayerLock ( uint serial ) => _playerLocks . TryRemove ( serial , out _ ) ;
2429
2530 #region LoginServer Operations
2631
@@ -309,175 +314,116 @@ public static async Task<bool> PasswordSaveAttempt(Aisling obj)
309314 /// Saves a player's state on disconnect or error
310315 /// Utilizes an active connection that self-heals if closed
311316 /// </summary>
312- public static async Task < bool > Save ( Aisling obj )
317+ public async Task < bool > Save ( Aisling obj , CancellationToken ct = default )
313318 {
314319 if ( obj == null ) return false ;
315320 if ( obj . Loading ) return false ;
316- var connection = ServerSetup . Instance . ServerSaveConnection ;
317321
318- try
322+ using ( await GetPlayerLock ( obj . Serial ) . LockAsync ( ) . ConfigureAwait ( false ) )
319323 {
320- _ = PlayerSaveRoutine ( obj , connection ) ;
321- }
322- catch ( Exception e )
323- {
324- SentrySdk . CaptureException ( e ) ;
325- }
326- finally
327- {
328- if ( connection . State != ConnectionState . Open )
324+ try
325+ {
326+ await PlayerSaveRoutine ( obj , ct ) . ConfigureAwait ( false ) ;
327+ return true ;
328+ }
329+ catch ( Exception e )
329330 {
330- Console . ForegroundColor = ConsoleColor . Blue ;
331- Console . WriteLine ( "Reconnecting Player Save-State" ) ;
332- ServerSetup . Instance . ServerSaveConnection = new SqlConnection ( ConnectionString ) ;
333- ServerSetup . Instance . ServerSaveConnection . Open ( ) ;
331+ SentrySdk . CaptureException ( e ) ;
332+ return false ;
334333 }
335334 }
336-
337- return true ;
338335 }
339336
340337 /// <summary>
341338 /// Saves all players states
342339 /// Utilizes an active connection that self-heals if closed
343340 /// </summary>
344- public async Task < bool > ServerSave ( List < Aisling > playerList )
341+ public async Task ServerSave ( List < Aisling > players , CancellationToken ct = default )
345342 {
346- if ( playerList . Count == 0 ) return false ;
343+ if ( players . Count == 0 ) return ;
344+ const int maxConcurrency = 10 ;
345+ using var throttler = new SemaphoreSlim ( maxConcurrency , maxConcurrency ) ;
346+
347+ var tasks = new List < Task > ( players . Count ) ;
347348
348- using ( await SaveLock . LockAsync ( ) )
349+ foreach ( var p in players )
349350 {
350- var connection = ServerSetup . Instance . ServerSaveConnection ;
351+ await throttler . WaitAsync ( ct ) . ConfigureAwait ( false ) ;
351352
352- try
353+ tasks . Add ( Task . Run ( async ( ) =>
353354 {
354- const int maxConcurrency = 10 ;
355- var semaphore = new SemaphoreSlim ( maxConcurrency ) ;
356- var tasks = new List < Task > ( ) ;
357-
358- foreach ( var player in playerList . Where ( p => p is { Loading : false } ) )
355+ try
359356 {
360- await semaphore . WaitAsync ( ) ;
361-
362- var task = PlayerSaveRoutine ( player , connection ) . ContinueWith ( _ => semaphore . Release ( ) ) ;
363- tasks . Add ( task ) ;
357+ await Save ( p , ct ) . ConfigureAwait ( false ) ;
364358 }
365-
366- await Task . WhenAll ( tasks ) ;
367- }
368- catch ( Exception e )
369- {
370- SentrySdk . CaptureException ( e ) ;
371- }
372- finally
373- {
374- if ( connection . State != ConnectionState . Open )
359+ finally
375360 {
376- Console . ForegroundColor = ConsoleColor . Blue ;
377- Console . WriteLine ( "Reconnecting Player Save-State" ) ;
378- ServerSetup . Instance . ServerSaveConnection = new SqlConnection ( ConnectionString ) ;
379- ServerSetup . Instance . ServerSaveConnection . Open ( ) ;
361+ throttler . Release ( ) ;
380362 }
381- }
382-
383- return true ;
363+ } , ct ) ) ;
384364 }
365+
366+ await Task . WhenAll ( tasks ) . ConfigureAwait ( false ) ;
385367 }
386368
387- private static Task PlayerSaveRoutine ( Aisling player , SqlConnection connection )
369+ private async Task PlayerSaveRoutine ( Aisling player , CancellationToken ct )
388370 {
389- if ( player . Client == null ) return Task . CompletedTask ;
390- player . Client . LastSave = DateTime . UtcNow ;
391- var dt = PlayerDataTable ( ) ;
392- var qDt = QuestDataTable ( ) ;
393- var cDt = ComboScrollDataTable ( ) ;
394- var iDt = ItemsDataTable ( ) ;
395- var skillDt = SkillDataTable ( ) ;
396- var spellDt = SpellDataTable ( ) ;
397- var buffDt = BuffsDataTable ( ) ;
398- var debuffDt = DeBuffsDataTable ( ) ;
399- dt = PlayerStatSave ( player , dt ) ;
400- qDt = PlayerQuestSave ( player , qDt ) ;
401- cDt = PlayerComboSave ( player , cDt ) ;
402- iDt = PlayerItemSave ( player , iDt ) ;
403- skillDt = PlayerSkillSave ( player , skillDt ) ;
404- spellDt = PlayerSpellSave ( player , spellDt ) ;
405- buffDt = PlayerBuffSave ( player , buffDt ) ;
406- debuffDt = PlayerDebuffSave ( player , debuffDt ) ;
407-
408- using ( var cmd = new SqlCommand ( "PlayerSave" , connection ) )
409- {
410- cmd . CommandType = CommandType . StoredProcedure ;
411- var param = cmd . Parameters . AddWithValue ( "@Players" , dt ) ;
412- param . SqlDbType = SqlDbType . Structured ;
413- param . TypeName = "dbo.PlayerType" ;
414- cmd . ExecuteNonQuery ( ) ;
415- }
416-
417- using ( var cmd2 = new SqlCommand ( "PlayerQuestSave" , connection ) )
418- {
419- cmd2 . CommandType = CommandType . StoredProcedure ;
420- var param2 = cmd2 . Parameters . AddWithValue ( "@Quests" , qDt ) ;
421- param2 . SqlDbType = SqlDbType . Structured ;
422- param2 . TypeName = "dbo.QuestType" ;
423- cmd2 . ExecuteNonQuery ( ) ;
424- }
371+ await using var conn = new SqlConnection ( ConnectionString ) ;
372+ await conn . OpenAsync ( ct ) . ConfigureAwait ( false ) ;
373+ await using var tx = ( SqlTransaction ) await conn . BeginTransactionAsync ( IsolationLevel . ReadCommitted , ct ) . ConfigureAwait ( false ) ;
425374
426- using ( var cmd3 = new SqlCommand ( "PlayerComboSave" , connection ) )
427- {
428- cmd3 . CommandType = CommandType . StoredProcedure ;
429- var param3 = cmd3 . Parameters . AddWithValue ( "@Combos" , cDt ) ;
430- param3 . SqlDbType = SqlDbType . Structured ;
431- param3 . TypeName = "dbo.ComboType" ;
432- cmd3 . ExecuteNonQuery ( ) ;
433- }
434-
435- using ( var cmd4 = new SqlCommand ( "ItemUpsert" , connection ) )
436- {
437- cmd4 . CommandType = CommandType . StoredProcedure ;
438- var param4 = cmd4 . Parameters . AddWithValue ( "@Items" , iDt ) ;
439- param4 . SqlDbType = SqlDbType . Structured ;
440- param4 . TypeName = "dbo.ItemType" ;
441- cmd4 . ExecuteNonQuery ( ) ;
442- }
443-
444- using ( var cmd5 = new SqlCommand ( "PlayerSaveSkills" , connection ) )
375+ try
445376 {
446- cmd5 . CommandType = CommandType . StoredProcedure ;
447- var param5 = cmd5 . Parameters . AddWithValue ( "@Skills" , skillDt ) ;
448- param5 . SqlDbType = SqlDbType . Structured ;
449- param5 . TypeName = "dbo.SkillType" ;
450- cmd5 . ExecuteNonQuery ( ) ;
377+ player . Client . LastSave = DateTime . UtcNow ;
378+ var dt = PlayerDataTable ( ) ;
379+ var qDt = QuestDataTable ( ) ;
380+ var cDt = ComboScrollDataTable ( ) ;
381+ var iDt = ItemsDataTable ( ) ;
382+ var skillDt = SkillDataTable ( ) ;
383+ var spellDt = SpellDataTable ( ) ;
384+ var buffDt = BuffsDataTable ( ) ;
385+ var debuffDt = DeBuffsDataTable ( ) ;
386+ dt = PlayerStatSave ( player , dt ) ;
387+ qDt = PlayerQuestSave ( player , qDt ) ;
388+ cDt = PlayerComboSave ( player , cDt ) ;
389+ iDt = PlayerItemSave ( player , iDt ) ;
390+ skillDt = PlayerSkillSave ( player , skillDt ) ;
391+ spellDt = PlayerSpellSave ( player , spellDt ) ;
392+ buffDt = PlayerBuffSave ( player , buffDt ) ;
393+ debuffDt = PlayerDebuffSave ( player , debuffDt ) ;
394+
395+ await ExecTvpAsync ( conn , tx , "PlayerSave" , "@Players" , "dbo.PlayerType" , dt , ct ) . ConfigureAwait ( false ) ;
396+ await ExecTvpAsync ( conn , tx , "PlayerQuestSave" , "@Quests" , "dbo.QuestType" , qDt , ct ) . ConfigureAwait ( false ) ;
397+ await ExecTvpAsync ( conn , tx , "PlayerComboSave" , "@Combos" , "dbo.ComboType" , cDt , ct ) . ConfigureAwait ( false ) ;
398+ await ExecTvpAsync ( conn , tx , "ItemUpsert" , "@Items" , "dbo.ItemType" , iDt , ct ) . ConfigureAwait ( false ) ;
399+ await ExecTvpAsync ( conn , tx , "PlayerSaveSkills" , "@Skills" , "dbo.SkillType" , skillDt , ct ) . ConfigureAwait ( false ) ;
400+ await ExecTvpAsync ( conn , tx , "PlayerSaveSpells" , "@Spells" , "dbo.SpellType" , spellDt , ct ) . ConfigureAwait ( false ) ;
401+ await ExecTvpAsync ( conn , tx , "BuffSave" , "@Buffs" , "dbo.BuffType" , buffDt , ct ) . ConfigureAwait ( false ) ;
402+ await ExecTvpAsync ( conn , tx , "DeBuffSave" , "@Debuffs" , "dbo.DebuffType" , debuffDt , ct ) . ConfigureAwait ( false ) ;
403+
404+ await tx . CommitAsync ( ct ) . ConfigureAwait ( false ) ;
451405 }
452-
453- using ( var cmd6 = new SqlCommand ( "PlayerSaveSpells" , connection ) )
406+ catch ( Exception e )
454407 {
455- cmd6 . CommandType = CommandType . StoredProcedure ;
456- var param6 = cmd6 . Parameters . AddWithValue ( "@Spells" , spellDt ) ;
457- param6 . SqlDbType = SqlDbType . Structured ;
458- param6 . TypeName = "dbo.SpellType" ;
459- cmd6 . ExecuteNonQuery ( ) ;
408+ await tx . RollbackAsync ( ct ) . ConfigureAwait ( false ) ;
409+ ServerSetup . EventsLogger ( $ "PlayerSave performed rollback!", LogLevel . Error ) ;
410+ SentrySdk . CaptureException ( e ) ;
460411 }
412+ }
461413
462- using ( var cmd7 = new SqlCommand ( "BuffSave" , connection ) )
414+ private static async Task ExecTvpAsync ( SqlConnection conn , SqlTransaction tx , string procName ,
415+ string paramName , string typeName , DataTable tvp , CancellationToken ct )
416+ {
417+ await using var cmd = new SqlCommand ( procName , conn , tx )
463418 {
464- cmd7 . CommandType = CommandType . StoredProcedure ;
465- var param7 = cmd7 . Parameters . AddWithValue ( "@Buffs" , buffDt ) ;
466- param7 . SqlDbType = SqlDbType . Structured ;
467- param7 . TypeName = "dbo.BuffType" ;
468- cmd7 . ExecuteNonQuery ( ) ;
469- }
419+ CommandType = CommandType . StoredProcedure
420+ } ;
470421
471- using ( var cmd8 = new SqlCommand ( "DeBuffSave" , connection ) )
472- {
473- cmd8 . CommandType = CommandType . StoredProcedure ;
474- var param8 = cmd8 . Parameters . AddWithValue ( "@Debuffs" , debuffDt ) ;
475- param8 . SqlDbType = SqlDbType . Structured ;
476- param8 . TypeName = "dbo.DebuffType" ;
477- cmd8 . ExecuteNonQuery ( ) ;
478- }
422+ var p = cmd . Parameters . AddWithValue ( paramName , tvp ) ;
423+ p . SqlDbType = SqlDbType . Structured ;
424+ p . TypeName = typeName ;
479425
480- return Task . CompletedTask ;
426+ await cmd . ExecuteNonQueryAsync ( ct ) . ConfigureAwait ( false ) ;
481427 }
482428
483429 private static DataTable PlayerStatSave ( Aisling obj , DataTable dt )
0 commit comments