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. Возвращаем остаток, если не всё удалось добавить // 3. Возвращаем остаток, если не всё удалось добавить
AutoSave(); // Автоматическое сохранение
return toAdd; return toAdd;
} }
@ -141,6 +142,7 @@ public class Inventar : Component
_cacheDirty = true; // Помечаем кэш как устаревший _cacheDirty = true; // Помечаем кэш как устаревший
OnChanged?.Invoke(); OnChanged?.Invoke();
OnItemRemoved?.Invoke( item ); OnItemRemoved?.Invoke( item );
AutoSave(); // Автоматическое сохранение
return true; return true;
} }
else else
@ -149,6 +151,7 @@ public class Inventar : Component
item.Count -= count; item.Count -= count;
_cacheDirty = true; // Помечаем кэш как устаревший _cacheDirty = true; // Помечаем кэш как устаревший
OnChanged?.Invoke(); OnChanged?.Invoke();
AutoSave(); // Автоматическое сохранение
return true; return true;
} }
} }
@ -181,6 +184,7 @@ public class Inventar : Component
item.OnEquipped(); item.OnEquipped();
OnEquipped?.Invoke( item ); OnEquipped?.Invoke( item );
OnChanged?.Invoke(); OnChanged?.Invoke();
AutoSave(); // Автоматическое сохранение
return true; return true;
} }
@ -219,6 +223,7 @@ public class Inventar : Component
item.OnUnEquipped(); item.OnUnEquipped();
OnUnEquipped?.Invoke( item ); OnUnEquipped?.Invoke( item );
OnChanged?.Invoke(); OnChanged?.Invoke();
AutoSave(); // Автоматическое сохранение
} }
} }
@ -285,4 +290,49 @@ public class Inventar : Component
return (float)Items.Count / MaxInventorySlots * 100f; 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. Проверьте логи на наличие ошибок сохранения

View File

@ -73,6 +73,13 @@ public sealed class NetworkManager : Component, Component.INetworkListener
/// </summary> /// </summary>
public void OnDisconnected( Connection channel ) public void OnDisconnected( Connection channel )
{ {
// Сохраняем инвентарь игрока перед отключением
var player = Dedugan.GetByID( channel.Id );
if ( player != null )
{
player.SaveInventoryOnExit();
}
Dedugan.InternalPlayers.Remove( Dedugan.GetByID( channel.Id ) ); Dedugan.InternalPlayers.Remove( Dedugan.GetByID( channel.Id ) );
} }
@ -81,7 +88,12 @@ public sealed class NetworkManager : Component, Component.INetworkListener
/// </summary> /// </summary>
public void OnBecomeInactive( Connection channel ) 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();
}
} }
/// <summary> /// <summary>

View File

@ -13,6 +13,10 @@ public sealed partial class Dedugan : Component
// Автоматическая стрельба // Автоматическая стрельба
private bool _isShooting = false; private bool _isShooting = false;
private TimeSince _lastShootTime = 0f; private TimeSince _lastShootTime = 0f;
// Периодическое сохранение
private TimeSince _lastAutoSave = 0f;
private const float AUTO_SAVE_INTERVAL = 60f; // Сохраняем каждые 60 секунд
void InventoryStart() void InventoryStart()
{ {
@ -26,6 +30,25 @@ public sealed partial class Dedugan : Component
Inventory.OnUnEquipped += OnItemUnEquipped; Inventory.OnUnEquipped += OnItemUnEquipped;
Inventory.OnItemAdded += OnItemAdded; Inventory.OnItemAdded += OnItemAdded;
Inventory.OnItemRemoved += OnItemRemoved; Inventory.OnItemRemoved += OnItemRemoved;
// Загружаем инвентарь из сохранения при старте
LoadInventoryFromSave();
}
/// <summary>
/// Загружает инвентарь из сохранения
/// </summary>
private void LoadInventoryFromSave()
{
if ( Inventory.HasSaveFile() )
{
Log.Info( "Загружаем инвентарь из сохранения..." );
Inventory.LoadFromSave();
}
else
{
Log.Info( "Файл сохранения инвентаря не найден, начинаем с пустым инвентарем" );
}
} }
private void OnItemEquipped( InventoryItem item ) private void OnItemEquipped( InventoryItem item )
@ -159,6 +182,17 @@ public sealed partial class Dedugan : Component
{ {
TryReloadWeapon(); TryReloadWeapon();
} }
// Периодическое автоматическое сохранение
if ( _lastAutoSave > AUTO_SAVE_INTERVAL )
{
if ( Inventory != null )
{
Log.Info( "Периодическое автоматическое сохранение инвентаря..." );
Sasalka.InventorySaveSystem.SaveInventory( Inventory );
}
_lastAutoSave = 0f;
}
} }
/// <summary> /// <summary>

View File

@ -116,6 +116,18 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork
DrawDebugGizmos(); DrawDebugGizmos();
} }
/// <summary>
/// Сохраняет инвентарь при выходе из игры
/// </summary>
public void SaveInventoryOnExit()
{
if ( Network.IsOwner && Inventory != null )
{
Log.Info( "Сохраняем инвентарь при выходе из игры..." );
Sasalka.InventorySaveSystem.SaveInventory( Inventory );
}
}
/// <summary> /// <summary>
/// Реализация интерфейса IUseContext /// Реализация интерфейса IUseContext
/// Возвращает список используемых предметов /// Возвращает список используемых предметов
@ -189,4 +201,10 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork
_bodyRotationDirty = true; // Помечаем, что тело повернулось _bodyRotationDirty = true; // Помечаем, что тело повернулось
} }
} }
protected override void OnDestroy()
{
// Сохраняем инвентарь при уничтожении объекта (закрытие игры)
SaveInventoryOnExit();
}
} }

View File

@ -1,5 +1,6 @@
@using Sandbox @using Sandbox
@using Sandbox.UI @using Sandbox.UI
@using Sasalka
@inherits PanelComponent @inherits PanelComponent
@namespace Sandbox @namespace Sandbox
@ -23,6 +24,31 @@
</div> </div>
} }
</div> </div>
<!-- Панель управления сохранением инвентаря -->
<div class="save-status">
<div class="save-indicator @( HasSaveFile ? "has-save" : "no-save" )">
<div class="save-text">@( HasSaveFile ? "Сохранение есть" : "Нет сохранения" )</div>
</div>
@if ( HasSaveFile )
{
<button class="load-button" onmousedown="LoadInventory">
<div class="button-text">Загрузить</div>
</button>
}
<button class="save-button" onmousedown="SaveInventory">
<div class="button-text">Сохранить</div>
</button>
@if ( HasSaveFile )
{
<button class="clear-button" onmousedown="ClearSave">
<div class="button-text">Очистить</div>
</button>
}
</div>
</root> </root>
<style> <style>
@ -98,6 +124,95 @@
.hidden { .hidden {
display: none; display: none;
} }
.save-status {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 200px;
.save-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
&.has-save {
background: rgba(76, 175, 80, 0.2);
color: #4CAF50;
border: 1px solid #4CAF50;
}
&.no-save {
background: rgba(244, 67, 54, 0.2);
color: #F44336;
border: 1px solid #F44336;
}
.save-text {
font-size: 14px;
font-weight: 500;
}
}
button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
}
&.save-button {
background: #2196F3;
color: white;
&:hover {
background: #1976D2;
}
}
&.load-button {
background: #4CAF50;
color: white;
&:hover {
background: #388E3C;
}
}
&.clear-button {
background: #F44336;
color: white;
&:hover {
background: #D32F2F;
}
}
.button-text {
font-size: 14px;
font-weight: 500;
}
}
}
</style> </style>
@code { @code {
@ -107,6 +222,10 @@
private float ReloadProgress { get; set; } = 0f; private float ReloadProgress { get; set; } = 0f;
private int TotalInInventory { get; set; } = 0; private int TotalInInventory { get; set; } = 0;
private bool HasWeapon { get; set; } = false; private bool HasWeapon { get; set; } = false;
// Переменные для системы сохранения
private bool HasSaveFile { get; set; } = false;
private TimeSince _lastCheck = 0f;
protected override void OnUpdate() protected override void OnUpdate()
{ {
@ -122,6 +241,47 @@
// Проверяем, есть ли оружие в руках (если MaxAmmo > 0, значит есть оружие) // Проверяем, есть ли оружие в руках (если MaxAmmo > 0, значит есть оружие)
HasWeapon = 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() protected override int BuildHash()
@ -136,8 +296,8 @@
hash.Add( IsReloading ); hash.Add( IsReloading );
hash.Add( ReloadProgress ); hash.Add( ReloadProgress );
hash.Add( HasWeapon ); hash.Add( HasWeapon );
hash.Add( HasSaveFile );
return hash.ToHashCode(); return hash.ToHashCode();
} }
} }