Adds support for syncing GameSaves
This commit is contained in:
parent
dbe298f2a7
commit
3bd0a35c61
@ -1 +1 @@
|
||||
Subproject commit 911d36bd01964ad7aeb0b4a37610402fd9845310
|
||||
Subproject commit 8e6c824c3bc847cc4749e53256b42f9a9207c20f
|
||||
@ -1 +1 @@
|
||||
Subproject commit 6da27f9b694e886d6c9805ba54848b0f0a922598
|
||||
Subproject commit 473474019bd2e3be623cd593da4c1e8c4e51fb80
|
||||
@ -129,6 +129,8 @@
|
||||
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */; };
|
||||
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */; };
|
||||
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; };
|
||||
BFE593CA21F3F8B7003412A6 /* GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593C921F3F8B7003412A6 /* GameSave.swift */; };
|
||||
BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593CB21F3F8C2003412A6 /* _GameSave.swift */; };
|
||||
BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; };
|
||||
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */; };
|
||||
@ -281,6 +283,8 @@
|
||||
BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverMenuButton.swift; sourceTree = "<group>"; };
|
||||
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesStoryboardSegue.swift; sourceTree = "<group>"; };
|
||||
BFE593C921F3F8B7003412A6 /* GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameSave.swift; sourceTree = "<group>"; };
|
||||
BFE593CB21F3F8C2003412A6 /* _GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _GameSave.swift; sourceTree = "<group>"; };
|
||||
BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFEF24F21F7DD4FB00454C62 /* SaveStateMigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveStateMigrationPolicy.swift; sourceTree = "<group>"; };
|
||||
BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBCDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -471,6 +475,7 @@
|
||||
BF5942791E09BC830051894B /* Game.swift */,
|
||||
BF59427A1E09BC830051894B /* GameCollection.swift */,
|
||||
BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */,
|
||||
BFE593C921F3F8B7003412A6 /* GameSave.swift */,
|
||||
BF59427B1E09BC830051894B /* SaveState.swift */,
|
||||
);
|
||||
path = Human;
|
||||
@ -484,6 +489,7 @@
|
||||
BF5942831E09BC8B0051894B /* _Game.swift */,
|
||||
BF5942841E09BC8B0051894B /* _GameCollection.swift */,
|
||||
BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */,
|
||||
BFE593CB21F3F8C2003412A6 /* _GameSave.swift */,
|
||||
BF5942851E09BC8B0051894B /* _SaveState.swift */,
|
||||
);
|
||||
path = Machine;
|
||||
@ -957,6 +963,7 @@
|
||||
BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */,
|
||||
BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */,
|
||||
BF59427E1E09BC830051894B /* Game.swift in Sources */,
|
||||
BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */,
|
||||
BF63A1A321A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift in Sources */,
|
||||
BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */,
|
||||
BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */,
|
||||
@ -965,6 +972,7 @@
|
||||
BF59428E1E09BCFB0051894B /* ImportController.swift in Sources */,
|
||||
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */,
|
||||
BF48F74E219A16DA00BC2FC1 /* SyncingServicesViewController.swift in Sources */,
|
||||
BFE593CA21F3F8B7003412A6 /* GameSave.swift in Sources */,
|
||||
BF63A1B521A4B76E00EE8F61 /* RecordVersionsViewController.swift in Sources */,
|
||||
BF6424831F5B8F3F00D6AB44 /* ListMenuViewController.swift in Sources */,
|
||||
BF1020E31F95B05B00313182 /* DeltaToDelta2.xcmappingmodel in Sources */,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18B75" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
@ -62,6 +62,7 @@
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
@ -109,6 +110,16 @@
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
@ -135,9 +146,10 @@
|
||||
<elements>
|
||||
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
|
||||
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="135"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="195"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="210"/>
|
||||
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
|
||||
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
|
||||
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
|
||||
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
|
||||
</elements>
|
||||
</model>
|
||||
60
Delta/Database/Model/Human/GameSave.swift
Normal file
60
Delta/Database/Model/Human/GameSave.swift
Normal file
@ -0,0 +1,60 @@
|
||||
//
|
||||
// GameSave.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/30/16.
|
||||
// Copyright (c) 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Harmony
|
||||
|
||||
@objc(GameSave)
|
||||
public class GameSave: _GameSave
|
||||
{
|
||||
public override func awakeFromInsert()
|
||||
{
|
||||
super.awakeFromInsert()
|
||||
|
||||
self.modifiedDate = Date()
|
||||
}
|
||||
}
|
||||
|
||||
extension GameSave: Syncable
|
||||
{
|
||||
public static var syncablePrimaryKey: AnyKeyPath {
|
||||
return \GameSave.identifier
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\GameSave.modifiedDate]
|
||||
}
|
||||
|
||||
public var syncableRelationships: Set<AnyKeyPath> {
|
||||
return [\GameSave.game]
|
||||
}
|
||||
|
||||
public var syncableFiles: Set<File> {
|
||||
guard let game = self.game else { return [] }
|
||||
|
||||
var files: Set<File> = [File(identifier: "gameSave", fileURL: game.gameSaveURL)]
|
||||
|
||||
if game.type == .gbc
|
||||
{
|
||||
let gameTimeSaveURL = game.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
|
||||
files.insert(File(identifier: "gameTimeSave", fileURL: gameTimeSaveURL))
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
public var syncableMetadata: [HarmonyMetadataKey : String] {
|
||||
guard let game = self.game else { return [:] }
|
||||
return [.gameID: game.identifier, .gameName: game.name]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.game?.name
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,8 @@ public class _Game: NSManagedObject
|
||||
|
||||
@NSManaged public var gameCollection: GameCollection?
|
||||
|
||||
@NSManaged public var gameSave: GameSave?
|
||||
|
||||
@NSManaged public var previewSaveState: SaveState?
|
||||
|
||||
@NSManaged public var saveStates: Set<SaveState>
|
||||
|
||||
26
Delta/Database/Model/Machine/_GameSave.swift
Normal file
26
Delta/Database/Model/Machine/_GameSave.swift
Normal file
@ -0,0 +1,26 @@
|
||||
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
|
||||
// Make changes to GameSave.swift instead.
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import DeltaCore
|
||||
|
||||
public class _GameSave: NSManagedObject
|
||||
{
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<GameSave> {
|
||||
return NSFetchRequest<GameSave>(entityName: "GameSave")
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@NSManaged public var identifier: String
|
||||
|
||||
@NSManaged public var modifiedDate: Date
|
||||
|
||||
// MARK: - Relationships
|
||||
|
||||
@NSManaged public var game: Game?
|
||||
|
||||
}
|
||||
|
||||
@ -66,6 +66,8 @@ class GameViewController: DeltaCore.GameViewController
|
||||
let game = self.game as? Game
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.managedObjectContextDidChange(with:)), name: .NSManagedObjectContextObjectsDidChange, object: game?.managedObjectContext)
|
||||
|
||||
self.emulatorCore?.saveHandler = { [weak self] _ in self?.updateGameSave() }
|
||||
|
||||
self.updateControllerSkin()
|
||||
self.updateControllers()
|
||||
}
|
||||
@ -501,6 +503,40 @@ private extension GameViewController
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Game Saves -
|
||||
/// Game Saves
|
||||
private extension GameViewController
|
||||
{
|
||||
func updateGameSave()
|
||||
{
|
||||
guard let game = self.game as? Game else { return }
|
||||
|
||||
DatabaseManager.shared.performBackgroundTask { (context) in
|
||||
let game = context.object(with: game.objectID) as! Game
|
||||
|
||||
if let gameSave = game.gameSave
|
||||
{
|
||||
gameSave.modifiedDate = Date()
|
||||
}
|
||||
else
|
||||
{
|
||||
let gameSave = GameSave(context: context)
|
||||
gameSave.identifier = game.identifier
|
||||
game.gameSave = gameSave
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error updating game save.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Save States -
|
||||
/// Save States
|
||||
extension GameViewController: SaveStatesViewControllerDelegate
|
||||
|
||||
@ -85,11 +85,18 @@ private extension GameSyncStatusViewController
|
||||
}
|
||||
}
|
||||
|
||||
let gameDataSource = RSTArrayTableViewDataSource<Game>(items: [self.game])
|
||||
gameDataSource.cellConfigurationHandler = { (cell, game, indexPath) in
|
||||
cell.textLabel?.text = NSLocalizedString("Game", comment: "")
|
||||
let gameDataSource = RSTArrayTableViewDataSource<NSManagedObject>(items: [self.game, self.game.gameSave].compactMap { $0 })
|
||||
gameDataSource.cellConfigurationHandler = { (cell, item, indexPath) in
|
||||
if item is Game
|
||||
{
|
||||
cell.textLabel?.text = NSLocalizedString("Game", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.textLabel?.text = NSLocalizedString("Game Save", comment: "")
|
||||
}
|
||||
|
||||
configure(cell, recordedObject: game)
|
||||
configure(cell, recordedObject: item)
|
||||
}
|
||||
|
||||
let saveStatesFetchRequest = SaveState.fetchRequest() as NSFetchRequest<SaveState>
|
||||
@ -129,7 +136,7 @@ private extension GameSyncStatusViewController
|
||||
|
||||
do
|
||||
{
|
||||
let recordedObjects = ([self.game!] + Array(self.game.saveStates) + Array(self.game.cheats)) as! [SyncableManagedObject]
|
||||
let recordedObjects = ([self.game, self.game.gameSave].compactMap { $0 } + Array(self.game.saveStates) + Array(self.game.cheats)) as! [SyncableManagedObject]
|
||||
let records = try SyncManager.shared.recordController.fetchRecords(for: recordedObjects)
|
||||
|
||||
for record in records
|
||||
@ -156,7 +163,7 @@ extension GameSyncStatusViewController
|
||||
|
||||
switch Section.allCases[section]
|
||||
{
|
||||
case .game: return NSLocalizedString("Game", comment: "")
|
||||
case .game: return nil
|
||||
case .saveStates: return NSLocalizedString("Save States", comment: "")
|
||||
case .cheats: return NSLocalizedString("Cheats", comment: "")
|
||||
}
|
||||
|
||||
@ -129,6 +129,7 @@ private extension SyncStatusViewController
|
||||
case let game as Game: conflictedGame = game
|
||||
case let saveState as SaveState: conflictedGame = saveState.game
|
||||
case let cheat as Cheat: conflictedGame = cheat.game
|
||||
case let gameSave as GameSave: conflictedGame = gameSave.game
|
||||
default: conflictedGame = nil
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ extension SyncManager
|
||||
case saveState = "SaveState"
|
||||
case controllerSkin = "ControllerSkin"
|
||||
case gameControllerInputMapping = "GameControllerInputMapping"
|
||||
case gameSave = "GameSave"
|
||||
|
||||
var localizedName: String {
|
||||
switch self
|
||||
@ -29,6 +30,7 @@ extension SyncManager
|
||||
case .saveState: return NSLocalizedString("Save State", comment: "")
|
||||
case .controllerSkin: return NSLocalizedString("Controller Skin", comment: "")
|
||||
case .gameControllerInputMapping: return NSLocalizedString("Game Controller Input Mapping", comment: "")
|
||||
case .gameSave: return NSLocalizedString("Game Save", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +119,7 @@ private extension SyncResultViewController
|
||||
switch recordType
|
||||
{
|
||||
case .game: title = NSLocalizedString("Game", comment: "")
|
||||
case .gameSave: title = NSLocalizedString("Game Save", comment: "")
|
||||
case .saveState, .cheat, .controllerSkin, .gameCollection, .gameControllerInputMapping: title = error.record.localizedName ?? recordType.localizedName
|
||||
}
|
||||
|
||||
@ -210,6 +211,7 @@ private extension SyncResultViewController
|
||||
switch recordType
|
||||
{
|
||||
case .game: group = .game(error.record.recordID)
|
||||
case .gameSave: group = .game(error.record.recordID)
|
||||
case .gameCollection: group = .gameCollection
|
||||
case .controllerSkin: group = .controllerSkin
|
||||
case .gameControllerInputMapping: group = .gameControllerInputMapping
|
||||
|
||||
2
External/Harmony
vendored
2
External/Harmony
vendored
@ -1 +1 @@
|
||||
Subproject commit 920a34311389cd4e3248b62f27f594058539b28b
|
||||
Subproject commit 935be63ed805013b8e16191036720b5e32be55e7
|
||||
Loading…
Reference in New Issue
Block a user