save
This commit is contained in:
@@ -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 Сохранение и загрузка
|
||||
|
||||
/// <summary>
|
||||
/// Автоматически сохраняет инвентарь при изменениях
|
||||
/// </summary>
|
||||
public void AutoSave()
|
||||
{
|
||||
if ( Network.IsOwner )
|
||||
{
|
||||
InventorySaveSystem.SaveInventory( this );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Загружает инвентарь из файла сохранения
|
||||
/// </summary>
|
||||
public void LoadFromSave()
|
||||
{
|
||||
if ( Network.IsOwner )
|
||||
{
|
||||
InventorySaveSystem.LoadInventory( this );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, существует ли файл сохранения
|
||||
/// </summary>
|
||||
public bool HasSaveFile()
|
||||
{
|
||||
return InventorySaveSystem.HasSaveFile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет файл сохранения
|
||||
/// </summary>
|
||||
public void DeleteSaveFile()
|
||||
{
|
||||
if ( Network.IsOwner )
|
||||
{
|
||||
InventorySaveSystem.DeleteSaveFile();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
186
Code/Inventory/InventorySaveSystem.cs
Normal file
186
Code/Inventory/InventorySaveSystem.cs
Normal file
@@ -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<SavedInventoryItem> Items { get; set; } = new();
|
||||
public Dictionary<string, string> 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";
|
||||
|
||||
/// <summary>
|
||||
/// Сохраняет инвентарь игрока в файл
|
||||
/// </summary>
|
||||
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}" );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Загружает инвентарь игрока из файла
|
||||
/// </summary>
|
||||
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<InventorySaveData>( json );
|
||||
|
||||
if ( saveData == null )
|
||||
{
|
||||
Log.Error( "Не удалось десериализовать данные сохранения" );
|
||||
return;
|
||||
}
|
||||
|
||||
// Очищаем текущий инвентарь
|
||||
inventory.ClearInventory();
|
||||
|
||||
// Словарь для связи ID предметов с объектами InventoryItem
|
||||
var itemIdMap = new Dictionary<string, InventoryItem>();
|
||||
|
||||
// Загружаем предметы в инвентарь
|
||||
foreach ( var savedItem in saveData.Items )
|
||||
{
|
||||
var itemDefinition = ResourceLibrary.Get<BaseItemDefinition>( 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<Inventar.InventorySlot>( slotName, out var slot ) &&
|
||||
itemIdMap.TryGetValue( itemId, out var item ) )
|
||||
{
|
||||
inventory.EquipItem( item );
|
||||
}
|
||||
}
|
||||
|
||||
// Уведомляем об изменениях
|
||||
inventory.NotifyChanged();
|
||||
|
||||
Log.Info( "Инвентарь успешно загружен" );
|
||||
}
|
||||
catch ( Exception ex )
|
||||
{
|
||||
Log.Error( $"Ошибка при загрузке инвентаря: {ex.Message}" );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, существует ли файл сохранения
|
||||
/// </summary>
|
||||
public static bool HasSaveFile()
|
||||
{
|
||||
return FileSystem.Data.FileExists( SAVE_FILE_NAME );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет файл сохранения
|
||||
/// </summary>
|
||||
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}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
118
Code/Inventory/README_SaveSystem.md
Normal file
118
Code/Inventory/README_SaveSystem.md
Normal file
@@ -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. Проверьте логи на наличие ошибок сохранения
|
||||
Reference in New Issue
Block a user