feat: 초기 커밋
- Progress Quest 6.4 Flutter 포팅 프로젝트 - 게임 루프, 상태 관리, UI 구현 - 캐릭터 생성, 인벤토리, 장비, 주문 시스템 - 시장/판매/구매 메커니즘
This commit is contained in:
33
lib/src/core/storage/save_manager.dart
Normal file
33
lib/src/core/storage/save_manager.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/save_data.dart';
|
||||
import 'package:askiineverdie/src/core/storage/save_repository.dart';
|
||||
import 'package:askiineverdie/src/core/storage/save_service.dart'
|
||||
show SaveFileInfo;
|
||||
|
||||
/// Coordinates saving/loading GameState using SaveRepository.
|
||||
class SaveManager {
|
||||
SaveManager(this._repo);
|
||||
|
||||
final SaveRepository _repo;
|
||||
static const String defaultFileName = 'progress.pqf';
|
||||
|
||||
/// Save current game state to disk. [fileName] may be absolute or relative.
|
||||
/// Returns outcome with error on failure.
|
||||
Future<SaveOutcome> saveState(GameState state, {String? fileName}) {
|
||||
final save = GameSave.fromState(state);
|
||||
return _repo.save(save, fileName ?? defaultFileName);
|
||||
}
|
||||
|
||||
/// Load game state from disk. [fileName] may be absolute (e.g., file picker).
|
||||
/// Returns outcome + optional state.
|
||||
Future<(SaveOutcome, GameState?)> loadState({String? fileName}) async {
|
||||
final (outcome, save) = await _repo.load(fileName ?? defaultFileName);
|
||||
if (!outcome.success || save == null) {
|
||||
return (outcome, null);
|
||||
}
|
||||
return (outcome, save.toState());
|
||||
}
|
||||
|
||||
/// 저장 파일 목록 조회
|
||||
Future<List<SaveFileInfo>> listSaves() => _repo.listSaves();
|
||||
}
|
||||
64
lib/src/core/storage/save_repository.dart
Normal file
64
lib/src/core/storage/save_repository.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:askiineverdie/src/core/model/save_data.dart';
|
||||
import 'package:askiineverdie/src/core/storage/save_service.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class SaveOutcome {
|
||||
const SaveOutcome.success([this.error]) : success = true;
|
||||
const SaveOutcome.failure(this.error) : success = false;
|
||||
|
||||
final bool success;
|
||||
final String? error;
|
||||
}
|
||||
|
||||
/// High-level save/load wrapper that resolves platform storage paths.
|
||||
class SaveRepository {
|
||||
SaveRepository() : _service = null;
|
||||
|
||||
SaveService? _service;
|
||||
|
||||
Future<void> _ensureService() async {
|
||||
if (_service != null) return;
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
_service = SaveService(baseDir: dir);
|
||||
}
|
||||
|
||||
Future<SaveOutcome> save(GameSave save, String fileName) async {
|
||||
try {
|
||||
await _ensureService();
|
||||
await _service!.save(save, fileName);
|
||||
return const SaveOutcome.success();
|
||||
} on FileSystemException catch (e) {
|
||||
final reason = e.osError?.message ?? e.message;
|
||||
return SaveOutcome.failure('Unable to save file: $reason');
|
||||
} catch (e) {
|
||||
return SaveOutcome.failure(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<(SaveOutcome, GameSave?)> load(String fileName) async {
|
||||
try {
|
||||
await _ensureService();
|
||||
final data = await _service!.load(fileName);
|
||||
return (const SaveOutcome.success(), data);
|
||||
} on FileSystemException catch (e) {
|
||||
final reason = e.osError?.message ?? e.message;
|
||||
return (SaveOutcome.failure('Unable to load save: $reason'), null);
|
||||
} on FormatException catch (e) {
|
||||
return (SaveOutcome.failure('Corrupted save file: ${e.message}'), null);
|
||||
} catch (e) {
|
||||
return (SaveOutcome.failure(e.toString()), null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 저장 파일 목록 조회
|
||||
Future<List<SaveFileInfo>> listSaves() async {
|
||||
try {
|
||||
await _ensureService();
|
||||
return await _service!.listSaves();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
84
lib/src/core/storage/save_service.dart
Normal file
84
lib/src/core/storage/save_service.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:askiineverdie/src/core/model/save_data.dart';
|
||||
|
||||
/// Persists GameSave as JSON compressed with GZipCodec.
|
||||
class SaveService {
|
||||
SaveService({required this.baseDir});
|
||||
|
||||
final Directory baseDir;
|
||||
final GZipCodec _gzip = GZipCodec();
|
||||
|
||||
Future<File> save(GameSave save, String fileName) async {
|
||||
final path = _resolvePath(fileName);
|
||||
final file = File(path);
|
||||
await file.parent.create(recursive: true);
|
||||
final jsonStr = jsonEncode(save.toJson());
|
||||
final bytes = utf8.encode(jsonStr);
|
||||
final compressed = _gzip.encode(bytes);
|
||||
return file.writeAsBytes(compressed);
|
||||
}
|
||||
|
||||
Future<GameSave> load(String fileName) async {
|
||||
final path = _resolvePath(fileName);
|
||||
final file = File(path);
|
||||
final compressed = await file.readAsBytes();
|
||||
final decompressed = _gzip.decode(compressed);
|
||||
final jsonStr = utf8.decode(decompressed);
|
||||
final map = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
return GameSave.fromJson(map);
|
||||
}
|
||||
|
||||
String _resolvePath(String fileName) {
|
||||
final normalized = fileName.endsWith('.pqf') ? fileName : '$fileName.pqf';
|
||||
final file = File(normalized);
|
||||
if (file.isAbsolute) return file.path;
|
||||
return '${baseDir.path}/$normalized';
|
||||
}
|
||||
|
||||
/// 저장 디렉토리의 모든 .pqf 파일 목록 반환
|
||||
Future<List<SaveFileInfo>> listSaves() async {
|
||||
if (!await baseDir.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final files = <SaveFileInfo>[];
|
||||
await for (final entity in baseDir.list()) {
|
||||
if (entity is File && entity.path.endsWith('.pqf')) {
|
||||
final stat = await entity.stat();
|
||||
final name = entity.uri.pathSegments.last;
|
||||
files.add(
|
||||
SaveFileInfo(
|
||||
fileName: name,
|
||||
fullPath: entity.path,
|
||||
modifiedAt: stat.modified,
|
||||
sizeBytes: stat.size,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 최근 수정된 파일 순으로 정렬
|
||||
files.sort((a, b) => b.modifiedAt.compareTo(a.modifiedAt));
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
||||
/// 저장 파일 정보
|
||||
class SaveFileInfo {
|
||||
const SaveFileInfo({
|
||||
required this.fileName,
|
||||
required this.fullPath,
|
||||
required this.modifiedAt,
|
||||
required this.sizeBytes,
|
||||
});
|
||||
|
||||
final String fileName;
|
||||
final String fullPath;
|
||||
final DateTime modifiedAt;
|
||||
final int sizeBytes;
|
||||
|
||||
/// 확장자 없는 표시용 이름
|
||||
String get displayName => fileName.replaceAll('.pqf', '');
|
||||
}
|
||||
Reference in New Issue
Block a user