From db31f362d185e5d1b0f88ee2aabfe891398ddb12 Mon Sep 17 00:00:00 2001 From: Oscar Date: Sun, 29 Jun 2025 12:39:23 +0300 Subject: [PATCH] save --- Code/Inventory/Inventar.cs | 50 +++++++ Code/Inventory/InventorySaveSystem.cs | 186 ++++++++++++++++++++++++++ Code/Inventory/README_SaveSystem.md | 118 ++++++++++++++++ Code/NetworkManager.cs | 14 +- Code/Player/Dedugan.Inventory.cs | 34 +++++ Code/Player/Dedugan.cs | 18 +++ Code/UI/GUI.razor | 162 +++++++++++++++++++++- 7 files changed, 580 insertions(+), 2 deletions(-) create mode 100644 Code/Inventory/InventorySaveSystem.cs create mode 100644 Code/Inventory/README_SaveSystem.md diff --git a/Code/Inventory/Inventar.cs b/Code/Inventory/Inventar.cs index 177d597..f0682dc 100644 --- a/Code/Inventory/Inventar.cs +++ b/Code/Inventory/Inventar.cs @@ -125,6 +125,7 @@ public class Inventar : Component } // 3. Возвращаем остаток, если не всё удалось добавить + AutoSave(); // Автоматическое сохранение return toAdd; } @@ -141,6 +142,7 @@ public class Inventar : Component _cacheDirty = true; // Помечаем кэш как устаревший OnChanged?.Invoke(); OnItemRemoved?.Invoke( item ); + AutoSave(); // Автоматическое сохранение return true; } else @@ -149,6 +151,7 @@ public class Inventar : Component item.Count -= count; _cacheDirty = true; // Помечаем кэш как устаревший OnChanged?.Invoke(); + AutoSave(); // Автоматическое сохранение return true; } } @@ -181,6 +184,7 @@ public class Inventar : Component item.OnEquipped(); OnEquipped?.Invoke( item ); OnChanged?.Invoke(); + AutoSave(); // Автоматическое сохранение return true; } @@ -219,6 +223,7 @@ public class Inventar : Component item.OnUnEquipped(); OnUnEquipped?.Invoke( item ); OnChanged?.Invoke(); + AutoSave(); // Автоматическое сохранение } } @@ -285,4 +290,49 @@ public class Inventar : Component return (float)Items.Count / MaxInventorySlots * 100f; } + + #region Сохранение и загрузка + + /// + /// Автоматически сохраняет инвентарь при изменениях + /// + public void AutoSave() + { + if ( Network.IsOwner ) + { + InventorySaveSystem.SaveInventory( this ); + } + } + + /// + /// Загружает инвентарь из файла сохранения + /// + public void LoadFromSave() + { + if ( Network.IsOwner ) + { + InventorySaveSystem.LoadInventory( this ); + } + } + + /// + /// Проверяет, существует ли файл сохранения + /// + public bool HasSaveFile() + { + return InventorySaveSystem.HasSaveFile(); + } + + /// + /// Удаляет файл сохранения + /// + public void DeleteSaveFile() + { + if ( Network.IsOwner ) + { + InventorySaveSystem.DeleteSaveFile(); + } + } + + #endregion } diff --git a/Code/Inventory/InventorySaveSystem.cs b/Code/Inventory/InventorySaveSystem.cs new file mode 100644 index 0000000..9db2216 --- /dev/null +++ b/Code/Inventory/InventorySaveSystem.cs @@ -0,0 +1,186 @@ +// Система сохранения инвентаря для s&box +using System; +using System.Collections.Generic; +using System.Linq; +using Sandbox; +using System.Text.Json; + +namespace Sasalka; + +public class InventorySaveData +{ + public List Items { get; set; } = new(); + public Dictionary EquippedItems { get; set; } = new(); // slot -> itemId +} + +public class SavedInventoryItem +{ + public string ItemDefinitionPath { get; set; } = ""; + public int Count { get; set; } = 1; + public int MagazineAmmo { get; set; } = 0; + public string Id { get; set; } = ""; +} + +public static class InventorySaveSystem +{ + private const string SAVE_FILE_NAME = "inventory_save.json"; + + /// + /// Сохраняет инвентарь игрока в файл + /// + public static void SaveInventory( Inventar inventory ) + { + if ( inventory == null ) return; + + try + { + var saveData = new InventorySaveData(); + + // Сохраняем предметы в инвентаре + foreach ( var item in inventory.Items ) + { + if ( item.Definition == null ) continue; + + var savedItem = new SavedInventoryItem + { + ItemDefinitionPath = item.Definition.ResourcePath, + Count = item.Count, + MagazineAmmo = item.MagazineAmmo, + Id = Guid.NewGuid().ToString() // Генерируем уникальный ID для предмета + }; + + saveData.Items.Add( savedItem ); + } + + // Сохраняем экипированные предметы + foreach ( var kvp in inventory.EquippedItems ) + { + var slot = kvp.Key; + var item = kvp.Value; + + // Находим соответствующий сохраненный предмет + var savedItem = saveData.Items.FirstOrDefault( x => + x.ItemDefinitionPath == item.Definition?.ResourcePath && + x.Count == item.Count && + x.MagazineAmmo == item.MagazineAmmo ); + + if ( savedItem != null ) + { + saveData.EquippedItems[slot.ToString()] = savedItem.Id; + } + } + + // Сериализуем и сохраняем в файл + var json = JsonSerializer.Serialize( saveData, new JsonSerializerOptions { WriteIndented = true } ); + FileSystem.Data.WriteAllText( SAVE_FILE_NAME, json ); + + Log.Info( "Инвентарь успешно сохранен" ); + } + catch ( Exception ex ) + { + Log.Error( $"Ошибка при сохранении инвентаря: {ex.Message}" ); + } + } + + /// + /// Загружает инвентарь игрока из файла + /// + public static void LoadInventory( Inventar inventory ) + { + if ( inventory == null ) return; + + try + { + if ( !FileSystem.Data.FileExists( SAVE_FILE_NAME ) ) + { + Log.Info( "Файл сохранения инвентаря не найден" ); + return; + } + + var json = FileSystem.Data.ReadAllText( SAVE_FILE_NAME ); + var saveData = JsonSerializer.Deserialize( json ); + + if ( saveData == null ) + { + Log.Error( "Не удалось десериализовать данные сохранения" ); + return; + } + + // Очищаем текущий инвентарь + inventory.ClearInventory(); + + // Словарь для связи ID предметов с объектами InventoryItem + var itemIdMap = new Dictionary(); + + // Загружаем предметы в инвентарь + foreach ( var savedItem in saveData.Items ) + { + var itemDefinition = ResourceLibrary.Get( savedItem.ItemDefinitionPath ); + if ( itemDefinition == null ) + { + Log.Warning( $"Не удалось загрузить определение предмета: {savedItem.ItemDefinitionPath}" ); + continue; + } + + var inventoryItem = new InventoryItem + { + Definition = itemDefinition, + Count = savedItem.Count, + MagazineAmmo = savedItem.MagazineAmmo + }; + + inventory.Items.Add( inventoryItem ); + itemIdMap[savedItem.Id] = inventoryItem; + } + + // Экипируем предметы + foreach ( var kvp in saveData.EquippedItems ) + { + var slotName = kvp.Key; + var itemId = kvp.Value; + + if ( Enum.TryParse( slotName, out var slot ) && + itemIdMap.TryGetValue( itemId, out var item ) ) + { + inventory.EquipItem( item ); + } + } + + // Уведомляем об изменениях + inventory.NotifyChanged(); + + Log.Info( "Инвентарь успешно загружен" ); + } + catch ( Exception ex ) + { + Log.Error( $"Ошибка при загрузке инвентаря: {ex.Message}" ); + } + } + + /// + /// Проверяет, существует ли файл сохранения + /// + public static bool HasSaveFile() + { + return FileSystem.Data.FileExists( SAVE_FILE_NAME ); + } + + /// + /// Удаляет файл сохранения + /// + public static void DeleteSaveFile() + { + try + { + if ( FileSystem.Data.FileExists( SAVE_FILE_NAME ) ) + { + FileSystem.Data.DeleteFile( SAVE_FILE_NAME ); + Log.Info( "Файл сохранения инвентаря удален" ); + } + } + catch ( Exception ex ) + { + Log.Error( $"Ошибка при удалении файла сохранения: {ex.Message}" ); + } + } +} \ No newline at end of file diff --git a/Code/Inventory/README_SaveSystem.md b/Code/Inventory/README_SaveSystem.md new file mode 100644 index 0000000..5e3af60 --- /dev/null +++ b/Code/Inventory/README_SaveSystem.md @@ -0,0 +1,118 @@ +# Система сохранения инвентаря + +## Описание + +Система автоматического сохранения и загрузки инвентаря игрока, включая экипированные предметы (одежда, оружие). + +## Функциональность + +### Автоматическое сохранение +- Инвентарь автоматически сохраняется при любых изменениях: + - Добавление предметов + - Удаление предметов + - Экипировка предметов + - Снятие предметов + +### Сохранение при выходе из игры +- **При отключении от сервера** - инвентарь сохраняется автоматически +- **При закрытии игры** - инвентарь сохраняется при уничтожении объекта игрока +- **При разрыве соединения** - дополнительное сохранение для защиты от потери данных + +### Периодическое сохранение +- **Каждые 60 секунд** - автоматическое сохранение для защиты от сбоев +- Защищает от потери прогресса при неожиданном закрытии игры + +### Автоматическая загрузка +- При входе в игру система автоматически проверяет наличие сохранения +- Если сохранение найдено, инвентарь загружается автоматически +- Если сохранения нет, начинается с пустым инвентарем + +### Сохраняемые данные +- Все предметы в инвентаре (название, количество, патроны в магазине) +- Экипированные предметы (одежда, оружие) +- Слоты экипировки + +## Использование + +### В игре +1. **UI панель**: В правом верхнем углу экрана отображается панель управления сохранением + - Зеленая индикация = есть сохранение + - Красная индикация = нет сохранения + - Кнопки: Сохранить, Загрузить, Очистить + +### Автоматическое сохранение +- **При изменениях** - сохранение происходит автоматически +- **При выходе** - сохранение при отключении/закрытии игры +- **Периодически** - каждые 60 секунд для защиты от сбоев + +## Технические детали + +### Файл сохранения +- Расположение: `data/inventory_save.json` +- Формат: JSON +- Структура: +```json +{ + "Items": [ + { + "ItemDefinitionPath": "path/to/item.resource", + "Count": 1, + "MagazineAmmo": 0, + "Id": "unique-id" + } + ], + "EquippedItems": { + "RightHand": "item-id", + "Body": "item-id" + } +} +``` + +### Классы системы +- `InventorySaveSystem` - статический класс для работы с сохранением +- `InventorySaveData` - модель данных для сохранения +- `SavedInventoryItem` - модель сохраненного предмета + +### Интеграция +- `Inventar` - добавлены методы `AutoSave()`, `LoadFromSave()`, `HasSaveFile()`, `DeleteSaveFile()` +- `Dedugan.Inventory.cs` - автоматическая загрузка при старте и периодическое сохранение +- `Dedugan.cs` - сохранение при выходе из игры (`OnDestroy`, `SaveInventoryOnExit()`) +- `NetworkManager.cs` - сохранение при отключении игрока (`OnDisconnected`, `OnBecomeInactive`) +- `GUI.razor` - интегрированная панель управления сохранением + +## Безопасность + +- Сохранение происходит только на клиенте (Network.IsOwner) +- Проверка валидности данных при загрузке +- Обработка ошибок с логированием +- Fallback на пустой инвентарь при ошибках +- **Множественные точки сохранения** для максимальной защиты данных + +## Совместимость + +- Работает со всеми типами предметов (оружие, одежда, патроны и т.д.) +- Поддерживает все слоты экипировки +- Сохраняет патроны в магазине оружия +- Совместима с существующей системой инвентаря + +## Устранение неполадок + +### Проблема: Инвентарь не загружается +1. Проверьте консоль на ошибки +2. Убедитесь, что файл `data/inventory_save.json` существует +3. Проверьте права доступа к папке data + +### Проблема: Предметы не сохраняются +1. Убедитесь, что вы владелец игрока (Network.IsOwner) +2. Проверьте, что предметы имеют валидные определения +3. Проверьте консоль на ошибки сериализации + +### Проблема: UI не отображается +1. Убедитесь, что панель сохранения интегрирована в GUI.razor +2. Проверьте, что стили добавлены в GUI.razor +3. Перезапустите игру + +### Проблема: Сохранение не работает при выходе +1. Проверьте, что метод `SaveInventoryOnExit()` вызывается +2. Убедитесь, что NetworkManager правильно обрабатывает отключения +3. Проверьте логи на наличие ошибок сохранения \ No newline at end of file diff --git a/Code/NetworkManager.cs b/Code/NetworkManager.cs index 672646c..ecaddb9 100644 --- a/Code/NetworkManager.cs +++ b/Code/NetworkManager.cs @@ -73,6 +73,13 @@ public sealed class NetworkManager : Component, Component.INetworkListener /// public void OnDisconnected( Connection channel ) { + // Сохраняем инвентарь игрока перед отключением + var player = Dedugan.GetByID( channel.Id ); + if ( player != null ) + { + player.SaveInventoryOnExit(); + } + Dedugan.InternalPlayers.Remove( Dedugan.GetByID( channel.Id ) ); } @@ -81,7 +88,12 @@ public sealed class NetworkManager : Component, Component.INetworkListener /// public void OnBecomeInactive( Connection channel ) { - // Optional: Handle any cleanup before player becomes fully inactive + // Сохраняем инвентарь игрока при разрыве соединения + var player = Dedugan.GetByID( channel.Id ); + if ( player != null ) + { + player.SaveInventoryOnExit(); + } } /// diff --git a/Code/Player/Dedugan.Inventory.cs b/Code/Player/Dedugan.Inventory.cs index 41c0a8f..b607062 100644 --- a/Code/Player/Dedugan.Inventory.cs +++ b/Code/Player/Dedugan.Inventory.cs @@ -13,6 +13,10 @@ public sealed partial class Dedugan : Component // Автоматическая стрельба private bool _isShooting = false; private TimeSince _lastShootTime = 0f; + + // Периодическое сохранение + private TimeSince _lastAutoSave = 0f; + private const float AUTO_SAVE_INTERVAL = 60f; // Сохраняем каждые 60 секунд void InventoryStart() { @@ -26,6 +30,25 @@ public sealed partial class Dedugan : Component Inventory.OnUnEquipped += OnItemUnEquipped; Inventory.OnItemAdded += OnItemAdded; Inventory.OnItemRemoved += OnItemRemoved; + + // Загружаем инвентарь из сохранения при старте + LoadInventoryFromSave(); + } + + /// + /// Загружает инвентарь из сохранения + /// + private void LoadInventoryFromSave() + { + if ( Inventory.HasSaveFile() ) + { + Log.Info( "Загружаем инвентарь из сохранения..." ); + Inventory.LoadFromSave(); + } + else + { + Log.Info( "Файл сохранения инвентаря не найден, начинаем с пустым инвентарем" ); + } } private void OnItemEquipped( InventoryItem item ) @@ -159,6 +182,17 @@ public sealed partial class Dedugan : Component { TryReloadWeapon(); } + + // Периодическое автоматическое сохранение + if ( _lastAutoSave > AUTO_SAVE_INTERVAL ) + { + if ( Inventory != null ) + { + Log.Info( "Периодическое автоматическое сохранение инвентаря..." ); + Sasalka.InventorySaveSystem.SaveInventory( Inventory ); + } + _lastAutoSave = 0f; + } } /// diff --git a/Code/Player/Dedugan.cs b/Code/Player/Dedugan.cs index b07d392..a1c7890 100644 --- a/Code/Player/Dedugan.cs +++ b/Code/Player/Dedugan.cs @@ -116,6 +116,18 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork DrawDebugGizmos(); } + /// + /// Сохраняет инвентарь при выходе из игры + /// + public void SaveInventoryOnExit() + { + if ( Network.IsOwner && Inventory != null ) + { + Log.Info( "Сохраняем инвентарь при выходе из игры..." ); + Sasalka.InventorySaveSystem.SaveInventory( Inventory ); + } + } + /// /// Реализация интерфейса IUseContext /// Возвращает список используемых предметов @@ -189,4 +201,10 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork _bodyRotationDirty = true; // Помечаем, что тело повернулось } } + + protected override void OnDestroy() + { + // Сохраняем инвентарь при уничтожении объекта (закрытие игры) + SaveInventoryOnExit(); + } } diff --git a/Code/UI/GUI.razor b/Code/UI/GUI.razor index 41e1cf0..4f4d3c8 100644 --- a/Code/UI/GUI.razor +++ b/Code/UI/GUI.razor @@ -1,5 +1,6 @@ @using Sandbox @using Sandbox.UI +@using Sasalka @inherits PanelComponent @namespace Sandbox @@ -23,6 +24,31 @@ } + + +
+
+
@( HasSaveFile ? "Сохранение есть" : "Нет сохранения" )
+
+ + @if ( HasSaveFile ) + { + + } + + + + @if ( HasSaveFile ) + { + + } +
@code { @@ -107,6 +222,10 @@ private float ReloadProgress { get; set; } = 0f; private int TotalInInventory { get; set; } = 0; private bool HasWeapon { get; set; } = false; + + // Переменные для системы сохранения + private bool HasSaveFile { get; set; } = false; + private TimeSince _lastCheck = 0f; protected override void OnUpdate() { @@ -122,6 +241,47 @@ // Проверяем, есть ли оружие в руках (если MaxAmmo > 0, значит есть оружие) HasWeapon = MaxAmmo > 0; + + // Проверяем наличие файла сохранения каждые 2 секунды + if ( _lastCheck > 2f ) + { + CheckSaveFile(); + _lastCheck = 0f; + } + } + + protected override void OnStart() + { + CheckSaveFile(); + } + + private void CheckSaveFile() + { + HasSaveFile = Sasalka.InventorySaveSystem.HasSaveFile(); + } + + private void SaveInventory() + { + if ( Dedugan.Local?.Inventory != null ) + { + Sasalka.InventorySaveSystem.SaveInventory( Dedugan.Local.Inventory ); + CheckSaveFile(); + } + } + + private void LoadInventory() + { + if ( Dedugan.Local?.Inventory != null ) + { + Sasalka.InventorySaveSystem.LoadInventory( Dedugan.Local.Inventory ); + CheckSaveFile(); + } + } + + private void ClearSave() + { + Sasalka.InventorySaveSystem.DeleteSaveFile(); + CheckSaveFile(); } protected override int BuildHash() @@ -136,8 +296,8 @@ hash.Add( IsReloading ); hash.Add( ReloadProgress ); hash.Add( HasWeapon ); + hash.Add( HasSaveFile ); return hash.ToHashCode(); } - }