diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index 3aaf1c9..a5aad37 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -207,6 +207,7 @@ BA436AF02BD437D900BE3E4F /* LocalStorage+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436AEF2BD437D900BE3E4F /* LocalStorage+Metadata.swift */; }; BA436AF22BD443A500BE3E4F /* RemoteStorage+Access.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436AF12BD443A500BE3E4F /* RemoteStorage+Access.swift */; }; BA436AF42BD4AB8400BE3E4F /* StorageHandler+Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436AF32BD4AB8400BE3E4F /* StorageHandler+Auth.swift */; }; + BA436AF62BD4AC2200BE3E4F /* StorageHandler+Conflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436AF52BD4AC2200BE3E4F /* StorageHandler+Conflict.swift */; }; BA443D3D2BAD9557009F0FFB /* RemoveSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D3C2BAD9557009F0FFB /* RemoveSystem.swift */; }; BA443D3F2BAD9774009F0FFB /* RemoveEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D3E2BAD9774009F0FFB /* RemoveEvent.swift */; }; BA443D422BAD9885009F0FFB /* DamageEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D412BAD9885009F0FFB /* DamageEventTests.swift */; }; @@ -490,6 +491,7 @@ BA436AEF2BD437D900BE3E4F /* LocalStorage+Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LocalStorage+Metadata.swift"; sourceTree = ""; }; BA436AF12BD443A500BE3E4F /* RemoteStorage+Access.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RemoteStorage+Access.swift"; sourceTree = ""; }; BA436AF32BD4AB8400BE3E4F /* StorageHandler+Auth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageHandler+Auth.swift"; sourceTree = ""; }; + BA436AF52BD4AC2200BE3E4F /* StorageHandler+Conflict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageHandler+Conflict.swift"; sourceTree = ""; }; BA443D3C2BAD9557009F0FFB /* RemoveSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveSystem.swift; sourceTree = ""; }; BA443D3E2BAD9774009F0FFB /* RemoveEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveEvent.swift; sourceTree = ""; }; BA443D412BAD9885009F0FFB /* DamageEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamageEventTests.swift; sourceTree = ""; }; @@ -1058,6 +1060,7 @@ children = ( BA436AE92BD42F5400BE3E4F /* StorageHandler.swift */, BA436AF32BD4AB8400BE3E4F /* StorageHandler+Auth.swift */, + BA436AF52BD4AC2200BE3E4F /* StorageHandler+Conflict.swift */, BAEC99FB2BD15AAB00E0C437 /* StorageDatabase.swift */, BA436AEB2BD42F7800BE3E4F /* LocalStorage.swift */, BA436AEF2BD437D900BE3E4F /* LocalStorage+Metadata.swift */, @@ -1774,6 +1777,7 @@ 3C9955AF2BA48FD200D33FA5 /* MeleeUnit.swift in Sources */, 5240D0AD2BB33D4C004F1486 /* PositionSystem.swift in Sources */, 5295A2022BA9FBD9005018A8 /* SceneManagerDelegate.swift in Sources */, + BA436AF62BD4AC2200BE3E4F /* StorageHandler+Conflict.swift in Sources */, 9B274DC42BD24B210062715C /* DamagePowerUp.swift in Sources */, 52DF5FE12BA3349600135367 /* TFTextures.swift in Sources */, 520062582BA8ED73000DBA30 /* HomeComponent.swift in Sources */, diff --git a/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift b/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift index d5954cd..0fc3540 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift @@ -181,9 +181,9 @@ extension Statistic { func merge(with that: Self) -> Self { let this = self - let largerPermanent = Double.maximum(this.permanentValue, that.permanentValue) - let largerCurrent = Double.maximum(this.currentValue, that.currentValue) - let largerMaxCurrent = Double.maximum(this.maximumCurrentValue, that.maximumCurrentValue) + let largerPermanent = max(this.permanentValue, that.permanentValue) + let largerCurrent = max(this.currentValue, that.currentValue) + let largerMaxCurrent = max(this.maximumCurrentValue, that.maximumCurrentValue) return Self(permanentValue: largerPermanent, currentValue: largerCurrent, @@ -191,9 +191,9 @@ extension Statistic { } static func merge(this: Self, that: Self) -> Self { - let largerPermanent = Double.maximum(this.permanentValue, that.permanentValue) - let largerCurrent = Double.maximum(this.currentValue, that.currentValue) - let largerMaxCurrent = Double.maximum(this.maximumCurrentValue, that.maximumCurrentValue) + let largerPermanent = max(this.permanentValue, that.permanentValue) + let largerCurrent = max(this.currentValue, that.currentValue) + let largerMaxCurrent = max(this.maximumCurrentValue, that.maximumCurrentValue) return Self(permanentValue: largerPermanent, currentValue: largerCurrent, diff --git a/TowerForge/TowerForge/StorageAPI/StorageHandler+Auth.swift b/TowerForge/TowerForge/StorageAPI/StorageHandler+Auth.swift index 7c199e2..cef9f6a 100644 --- a/TowerForge/TowerForge/StorageAPI/StorageHandler+Auth.swift +++ b/TowerForge/TowerForge/StorageAPI/StorageHandler+Auth.swift @@ -62,6 +62,8 @@ extension StorageHandler { /// Returns true if re-login success, false otherwise func onReLogin(completion: @escaping (Bool) -> Void) { + // Executed upon confirmation that both types of data exist remotely + // 1. Load metadata from firebase RemoteStorage.loadMetadataFromFirebase(player: Self.currentPlayerId) { remoteMetadata, _ in guard let remoteMetadata = remoteMetadata else { Logger.log("RELOGIN ERROR: REMOTE METADATA NOT FOUND") @@ -69,6 +71,7 @@ extension StorageHandler { return } + // 2. Load statistics from firebase RemoteStorage.loadDatabaseFromFirebase(player: Self.currentPlayerId) { remoteStorage, _ in guard let remoteStorage = remoteStorage else { Logger.log("RELOGIN ERROR: REMOTE STORAGE NOT FOUND") @@ -76,26 +79,19 @@ extension StorageHandler { return } - guard let finalStorage = StatisticsDatabase.merge(this: remoteStorage, that: self.statisticsDatabase) else { - Logger.log("RELOGIN ERROR: REMOTE STORAGE NOT FOUND") - completion(false) - return - } - - LocalStorage.saveDatabaseToLocalStorage(finalStorage) - RemoteStorage.saveDatabaseToFirebase(player: Self.currentPlayerId, - with: finalStorage) { saveStorageSuccess in - if !saveStorageSuccess { + // 3. Resolve conflict between remote statistics and current statistics + Self.resolveConflict(this: remoteStorage, that: self.statisticsDatabase) { resolvedStats in + guard let finalStorage = resolvedStats else { + Logger.log("RELOGIN ERROR: CONFLICT RESOLUTION FAILURE") completion(false) return } - self.metadata = remoteMetadata - RemoteStorage.saveMetadataToFirebase(player: Self.currentPlayerId, with: self.metadata) { - saveMetadataSuccess in + // 4. Update current instance to resolve storage + self.statisticsDatabase = finalStorage - completion(saveMetadataSuccess) - } + // 4. Save newly resolved storage universally + self.save() } } } diff --git a/TowerForge/TowerForge/StorageAPI/StorageHandler+Conflict.swift b/TowerForge/TowerForge/StorageAPI/StorageHandler+Conflict.swift new file mode 100644 index 0000000..71f7897 --- /dev/null +++ b/TowerForge/TowerForge/StorageAPI/StorageHandler+Conflict.swift @@ -0,0 +1,77 @@ +// +// StorageHandler+Conflict.swift +// TowerForge +// +// Created by Rubesh on 21/4/24. +// + +import Foundation + +/// This extension adds conflict resolution methods to StorageHandler +extension StorageHandler { + + /// Returns the StatisticsDatabase from the location that corresponds to the most recent save. + static func getLocationWithLatestMetadata(completion: @escaping (StorageLocation?) -> Void) { + RemoteStorage.loadMetadataFromFirebase(player: Self.currentPlayerId) { remoteMetadata, remoteError in + let localMetadata = LocalStorage.loadMetadataFromLocalStorage() + + // Handle errors or nil cases + if let remoteError = remoteError { + Logger.log("Error occurred retrieving metadata: \(remoteError)", self) + } + + switch (remoteMetadata, localMetadata) { + case (_?, nil): + completion(.Remote) + case (nil, _?): + completion(.Local) + case (let remote?, let local?): + completion(remote > local ? .Remote : .Local) + default: + completion(nil) + } + } + } + + static func loadLatest(completion: @escaping (StatisticsDatabase?) -> Void) { + Self.getLocationWithLatestMetadata { location in + switch location { + + case .Local: + if let stats = LocalStorage.loadDatabaseFromLocalStorage() { + completion(stats) + } else { + Logger.log("Error: Failed to load local database.", self) + completion(nil) + } + + case .Remote: + RemoteStorageManager.loadDatabaseFromFirebase { statsData, error in + if let error = error { + Logger.log("Error occurred loading from database: \(error)", self) + completion(nil) + } else { + completion(statsData) + } + } + + default: + Logger.log("No valid metadata found, cannot determine latest storage.", self) + completion(nil) + } + } + } + + static func resolveConflict(this: StatisticsDatabase, + that: StatisticsDatabase, + completion: @escaping (StatisticsDatabase?) -> Void) { + + switch CONFLICT_RESOLUTION { + case .MERGE: + completion(StatisticsDatabase.merge(this: this, that: that)) + case .KEEP_LATEST_ONLY: + Self.loadLatest { completion($0) } + } + } + +} diff --git a/TowerForge/TowerForge/StorageAPI/StorageHandler.swift b/TowerForge/TowerForge/StorageAPI/StorageHandler.swift index 33ffd4d..5797806 100644 --- a/TowerForge/TowerForge/StorageAPI/StorageHandler.swift +++ b/TowerForge/TowerForge/StorageAPI/StorageHandler.swift @@ -13,6 +13,7 @@ class StorageHandler: AuthenticationDelegate { static let fileName = Constants.LOCAL_STORAGE_FILE_NAME static let metadataName = Constants.METADATA_FILE_NAME + static var CONFLICT_RESOLUTION: StorageConflictResolution { Constants.CONFLICT_RESOLTION } static var currentPlayerId: String { Constants.CURRENT_PLAYER_ID } static var currentDeviceId: String { Constants.CURRENT_DEVICE_ID }