diff --git a/Server/Components/Pages/GamePage.razor b/Server/Components/Pages/GamePage.razor index fae5361..a01562c 100644 --- a/Server/Components/Pages/GamePage.razor +++ b/Server/Components/Pages/GamePage.razor @@ -153,3 +153,10 @@ + +@code { + private async Task Logout() + { + await LogoutAsync(); + } +} diff --git a/Server/Components/Pages/GamePage.razor.cs b/Server/Components/Pages/GamePage.razor.cs index 0b550e6..de5ffa6 100644 --- a/Server/Components/Pages/GamePage.razor.cs +++ b/Server/Components/Pages/GamePage.razor.cs @@ -1,6 +1,5 @@ using Fracture.Server.Components.Popups; using Fracture.Server.Components.UI; -using Fracture.Server.Modules.Items.Models; using Fracture.Server.Modules.MapGenerator.Models.Map; using Fracture.Server.Modules.MapGenerator.UI.Models; using Fracture.Server.Modules.Pathfinding.Models; @@ -9,16 +8,12 @@ namespace Fracture.Server.Components.Pages; -public partial class GamePage +public partial class GamePage : IAsyncDisposable { private Dictionary _mapPopupParameters = null!; - private PopupContainer _popup = null!; - public string BackgroundImage { get; set; } = string.Empty; - private readonly MapDisplayOptions _mapDisplayOptions = new(); - private List? Path { get; set; } protected override async Task OnInitializedAsync() @@ -34,6 +29,12 @@ protected override async Task OnInitializedAsync() await MovementService.InitializeAsync(); BackgroundImage = GetBackgroundImagePath(); } + else + { + //Reset dla nowego gracza + await MovementService.ReloadPlayerPositionAsync(); + BackgroundImage = GetBackgroundImagePath(); + } MovementService.OnMapEntered += async (sender, args) => { @@ -82,6 +83,8 @@ protected override async Task OnInitializedAsync() { { "MapDisplayData", _mapDisplayOptions }, }; + //Autosave Pozycji Gracza + MovementService.StartAutosave(); await base.OnInitializedAsync(); } @@ -110,10 +113,18 @@ private async Task LoadUserAsync() return true; } - private void Logout() + private async Task LogoutAsync() { + await MovementService.SavePlayerPositionAsync(); + await ProtectedSessionStore.DeleteAsync("username"); NavigationManager.NavigateTo("/home"); - ProtectedSessionStore.DeleteAsync("username"); + } + + public async ValueTask DisposeAsync() + { + MovementService.StopAutosave(); + await MovementService.SavePlayerPositionAsync(); + GC.SuppressFinalize(this); } private string GetBackgroundImagePath() diff --git a/Server/Migrations/FractureDbContextModelSnapshot.cs b/Server/Migrations/FractureDbContextModelSnapshot.cs index 560be54..37c031a 100644 --- a/Server/Migrations/FractureDbContextModelSnapshot.cs +++ b/Server/Migrations/FractureDbContextModelSnapshot.cs @@ -114,6 +114,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("TEXT"); + b.Property("PlayerPosition") + .HasColumnType("TEXT"); + b.Property("Username") .IsRequired() .HasColumnType("TEXT"); diff --git a/Server/Modules/Database/IUsersRepository.cs b/Server/Modules/Database/IUsersRepository.cs index c528ca6..0e43c2b 100644 --- a/Server/Modules/Database/IUsersRepository.cs +++ b/Server/Modules/Database/IUsersRepository.cs @@ -7,4 +7,5 @@ public interface IUsersRepository public Task AddUserAsync(User user); public Task GetUserAsync(int id); public Task GetUserAsync(string username); + public Task SaveAsync(); } diff --git a/Server/Modules/Database/UsersRepository.cs b/Server/Modules/Database/UsersRepository.cs index 7bc2104..0425b2b 100644 --- a/Server/Modules/Database/UsersRepository.cs +++ b/Server/Modules/Database/UsersRepository.cs @@ -38,4 +38,9 @@ public async Task AddUserAsync(User user) .FirstOrDefaultAsync(); return user; } + + public async Task SaveAsync() + { + await _dbContext.SaveChangesAsync(); + } } diff --git a/Server/Modules/Users/Models/User.cs b/Server/Modules/Users/Models/User.cs index 11df551..69b8e40 100644 --- a/Server/Modules/Users/Models/User.cs +++ b/Server/Modules/Users/Models/User.cs @@ -13,6 +13,11 @@ public class User public required string Username { get; set; } + /// + /// Pozycja gracza w formacie "X,Y" + /// + public string? PlayerPosition { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; [InverseProperty(nameof(Item.CreatedBy))] diff --git a/Server/Modules/Users/Services/MovementService.cs b/Server/Modules/Users/Services/MovementService.cs index 971d9ce..2556cf3 100644 --- a/Server/Modules/Users/Services/MovementService.cs +++ b/Server/Modules/Users/Services/MovementService.cs @@ -4,6 +4,7 @@ using Fracture.Server.Modules.Items.Services; using Fracture.Server.Modules.MapGenerator.Models.Map; using Fracture.Server.Modules.MapGenerator.Services; +using Microsoft.Extensions.Logging; namespace Fracture.Server.Modules.Users.Services; @@ -14,16 +15,26 @@ public class MovementService private readonly IItemsRepository _itemsRepository; private readonly UserService _userService; private readonly ItemDropStateService _itemDropState; + private readonly IUsersRepository _usersRepository; + private readonly ILogger _logger; private Position? _pendingPickupPosition; private Item? _pendingPickupItem; + // Autosave fields + private CancellationTokenSource? _autosaveCts; + private int _lastSavedX; + private int _lastSavedY; + private const int AUTOSAVE_INTERVAL_MS = 120000; // 2 minuty + public MovementService( MapManagerService mapManagerService, IItemGenerator itemGenerator, IItemsRepository itemsRepository, UserService userService, - ItemDropStateService itemDropState + ItemDropStateService itemDropState, + IUsersRepository usersRepository, + ILogger logger ) { _mapManagerService = mapManagerService; @@ -31,6 +42,8 @@ ItemDropStateService itemDropState _itemsRepository = itemsRepository; _userService = userService; _itemDropState = itemDropState; + _usersRepository = usersRepository; + _logger = logger; } public Map? CurrentMap { get; private set; } @@ -49,16 +62,118 @@ public async Task InitializeAsync() _mapManagerService.GetWorldMap() ?? throw new InvalidOperationException("Map cannot be loaded, critical error"); - var start = CurrentMap.GetRandomWalkableNode(); - CurrentX = start.X; - CurrentY = start.Y; + // Wczytaj zapisaną pozycję gracza + await LoadPlayerPositionAsync(); + + OnMapEntered?.Invoke(this, (CurrentMap, new Position(CurrentX, CurrentY))); - if (_userService.User != null) + // Uruchom autosave automatycznie + StartAutosave(); + } + + /// + /// Wczytuje pozycję gracza z bazy danych + /// + public async Task LoadPlayerPositionAsync() + { + if (_userService.User?.PlayerPosition is null) { - await _itemDropState.EnsureLoadedAsync(_userService.User.Id); + // Jeśli nie ma zapisanej pozycji, ustaw pozycję startową + CurrentX = CurrentMap?.GetRandomWalkableNode().X ?? 0; + CurrentY = CurrentMap?.GetRandomWalkableNode().Y ?? 0; + return; } - OnMapEntered?.Invoke(this, new(CurrentMap, new Position(CurrentX, CurrentY))); + var parts = _userService.User.PlayerPosition.Split(','); + if ( + parts.Length == 2 + && int.TryParse(parts[0], out var x) + && int.TryParse(parts[1], out var y) + ) + { + if (CanMove(x, y)) + { + CurrentX = x; + CurrentY = y; + _lastSavedX = x; + _lastSavedY = y; + _logger.LogInformation($"Loaded player position: ({x}, {y})"); + } + else + { + // Pozycja nie jest walkable - losuj nową + var randomPos = CurrentMap.GetRandomWalkableNode(); + CurrentX = randomPos.X; + CurrentY = randomPos.Y; + _lastSavedX = randomPos.X; + _lastSavedY = randomPos.Y; + } + } + } + + /// + /// Zapisuje aktualną pozycję gracza do bazy danych + /// + public async Task SavePlayerPositionAsync() + { + if (_userService.User is null) + return; + + _userService.User.PlayerPosition = $"{CurrentX},{CurrentY}"; + await _usersRepository.SaveAsync(); + } + + /// + /// Uruchamia autosave pozycji gracza co 2 minuty + /// + public void StartAutosave() + { + _autosaveCts = new CancellationTokenSource(); + _ = AutosavePositionAsync(_autosaveCts.Token); + } + + /// + /// Zatrzymuje autosave + /// + public void StopAutosave() + { + _autosaveCts?.Cancel(); + _autosaveCts?.Dispose(); + _autosaveCts = null; + } + + /// + /// Pętla autosave'a + /// + private async Task AutosavePositionAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(AUTOSAVE_INTERVAL_MS, cancellationToken); + if (CurrentX != _lastSavedX || CurrentY != _lastSavedY) + { + await SavePlayerPositionAsync(); + _lastSavedX = CurrentX; + _lastSavedY = CurrentY; + _logger.LogInformation( + $"Autosave triggered - position changed: ({_lastSavedX}, {_lastSavedY})" + ); + } + else + { + _logger.LogInformation("Autosave skipped - position unchanged"); + } + } + } + + /// + /// Przeładowuje pozycję gracza (dla nowego użytkownika) + /// + public async Task ReloadPlayerPositionAsync() + { + _lastSavedX = CurrentX; + _lastSavedY = CurrentY; + await LoadPlayerPositionAsync(); } public bool CanMove(int x, int y) diff --git a/Server/Modules/Users/Services/UserService.cs b/Server/Modules/Users/Services/UserService.cs index ae0e9d2..ec31838 100644 --- a/Server/Modules/Users/Services/UserService.cs +++ b/Server/Modules/Users/Services/UserService.cs @@ -6,7 +6,7 @@ namespace Fracture.Server.Modules.Users.Services; -public class UserService(IItemsRepository _itemsRepository) +public class UserService(IItemsRepository _itemsRepository, IUsersRepository _usersRepository) { public User? User { get; private set; } @@ -77,4 +77,20 @@ public bool IsEquipped(Item item) { return Equipment.Contains(item); } + + public async Task UpdatePlayerPositionAsync(int userId, string position) + { + var user = await _usersRepository.GetUserAsync(userId); + if (user != null) + { + user.PlayerPosition = position; + await _usersRepository.SaveAsync(); + } + } + + public async Task GetPlayerPositionAsync(int userId) + { + var user = await _usersRepository.GetUserAsync(userId); + return user?.PlayerPosition; + } }