This commit is contained in:
Oscar
2025-06-29 12:39:23 +03:00
parent eaeb0c45ac
commit db31f362d1
7 changed files with 580 additions and 2 deletions

View File

@@ -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
}

View 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}" );
}
}
}

View 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. Проверьте логи на наличие ошибок сохранения