Skip to content

Commit

Permalink
Save & Load Support (#24)
Browse files Browse the repository at this point in the history
* prototype save support

* make proximity a mating factor again

* move TankSettings struct into structs file

* ensure food nodes load from saves full-sized

* limit tank scene to 60 fps

* create core data models

* create NSManagedObject subclasses

* recreate data model

* saving/loading tanks to/from core data works so far

* whoops, forgot to save tank settings to data store

* previous save is loaded automatically on app launch

* add a fade-in on initial load of saved tank

* turn off scene debug display

* reorganize files

* ensure only one copy of a tank is saved to storage. at a time

* rename some methods

* set up timer to auto-save tank every 10 seconds
  • Loading branch information
amiantos committed Oct 6, 2019
1 parent d32eda3 commit 403a9e4
Show file tree
Hide file tree
Showing 77 changed files with 2,233 additions and 2,758 deletions.
12 changes: 0 additions & 12 deletions Aeon Garden Shared/CoreData/AeonDataManager.swift

This file was deleted.

197 changes: 197 additions & 0 deletions Aeon Garden Shared/Models/Core Data/CoreDataStore.swift
@@ -0,0 +1,197 @@
//
// CoreDataStore.swift
// Aeon Garden
//
// Created by Brad Root on 10/4/19.
// Copyright © 2019 Brad Root. All rights reserved.
//

import CoreData
import Foundation

class CoreDataStore {
static let standard: CoreDataStore = CoreDataStore()

var mainManagedObjectContext: NSManagedObjectContext
var persistentContainer: NSPersistentContainer

init() {
persistentContainer = {
let container = NSPersistentContainer(name: "AeonGarden")
container.loadPersistentStores(completionHandler: { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
mainManagedObjectContext = persistentContainer.viewContext
}

deinit {
self.saveContext()
}

func saveContext() {
if mainManagedObjectContext.hasChanges {
do {
try mainManagedObjectContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
// You should not use this function in a shipping application, although it
// may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

extension CoreDataStore: DataStoreProtocol {

// MARK: - Creature Favorites
func saveCreature(_ creature: Creature) {
fatalError()
}

func deleteCreature(_ creature: Creature) {
fatalError()
}

func getCreatures(completion: @escaping ([Creature]) -> Void) {
fatalError()
}

// MARK: - Tanks

func getTanks(completion: @escaping ([Tank]) -> Void) {
mainManagedObjectContext.perform {
do {
let fetchRequest: NSFetchRequest<ManagedTank> = ManagedTank.fetchRequest()
let managedTanks = try self.mainManagedObjectContext.fetch(fetchRequest) as [ManagedTank]
var tanks: [Tank] = []
for managedTank in managedTanks {
tanks.append(managedTank.toStruct())
}
completion(tanks)
} catch {
completion([])
}
}
}

func saveTank(_ tank: Tank) {
mainManagedObjectContext.perform {
do {
// Check for tank in storage by UUID
var storedTank: ManagedTank?
let fetchRequest: NSFetchRequest<ManagedTank> = ManagedTank.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "uuid == %@", tank.uuid.uuidString)
if let tank = try? self.mainManagedObjectContext.fetch(fetchRequest).first {
storedTank = tank
} else {
storedTank = ManagedTank(context: self.mainManagedObjectContext)
}

guard let managedTank = storedTank else {
Log.error("Could not load or create new managed tank object!")
return
}

managedTank.timestamp = Date()
managedTank.birthCount = Int16(tank.birthCount)
managedTank.deathCount = Int16(tank.deathCount)
managedTank.tankTime = tank.tankTime
managedTank.uuid = tank.uuid

var managedCreatures: [ManagedTankCreature] = []
for creature in tank.creatures {
let managedTankCreature = ManagedTankCreature(context: self.mainManagedObjectContext)
managedTankCreature.uuid = creature.uuid
managedTankCreature.firstName = creature.firstName
managedTankCreature.lastName = creature.lastName

managedTankCreature.currentHealth = creature.currentHealth
managedTankCreature.lifeTime = creature.lifeTime

managedTankCreature.isFavorite = creature.isFavorite

managedTankCreature.movementSpeed = creature.movementSpeed
managedTankCreature.turnSpeed = creature.turnSpeed
managedTankCreature.sizeModifier = creature.sizeModifier
managedTankCreature.primaryHue = creature.primaryHue

managedTankCreature.positionX = creature.positionX
managedTankCreature.positionY = creature.positionY

var managedLimbs: [ManagedLimb] = []
for limb in creature.limbs {
let managedLimb = ManagedLimb(context: self.mainManagedObjectContext)
managedLimb.shape = limb.shape.rawValue
managedLimb.hue = limb.hue
managedLimb.blend = limb.blend
managedLimb.brightness = limb.brightness
managedLimb.saturation = limb.saturation
managedLimb.limbWidth = Int16(limb.limbWidth)

managedLimb.wiggleFactor = limb.wiggleFactor
managedLimb.wiggleMoveFactor = limb.wiggleMoveFactor
managedLimb.wiggleMoveBackFactor = limb.wiggleMoveBackFactor
managedLimb.wiggleActionDuration = limb.wiggleActionDuration
managedLimb.wiggleActionBackDuration = limb.wiggleActionBackDuration
managedLimb.wiggleActionMovementDuration = limb.wiggleActionMovementDuration
managedLimb.wiggleActionMovementBackDuration = limb.wiggleActionMovementBackDuration

managedLimb.limbzRotation = limb.limbzRotation

managedLimb.positionX = limb.positionX
managedLimb.positionY = limb.positionY

managedLimbs.append(managedLimb)
}
managedTankCreature.limbs = NSSet(array: managedLimbs)

managedCreatures.append(managedTankCreature)
}
managedTank.creatures = NSSet(array: managedCreatures)

var managedBubbles: [ManagedBubble] = []
for bubble in tank.bubbles {
let managedBubble = ManagedBubble(context: self.mainManagedObjectContext)
managedBubble.positionY = bubble.positionY
managedBubble.positionX = bubble.positionX
managedBubbles.append(managedBubble)
}
managedTank.bubbles = NSSet(array: managedBubbles)

var managedFoods: [ManagedFood] = []
for food in tank.food {
let managedFood = ManagedFood(context: self.mainManagedObjectContext)
managedFood.positionY = food.positionY
managedFood.positionX = food.positionX
managedFoods.append(managedFood)
}
managedTank.food = NSSet(array: managedFoods)

let managedTankSettings = ManagedTankSettings(context: self.mainManagedObjectContext)
managedTankSettings.foodMaxAmount = Int16(tank.tankSettings.foodMaxAmount)
managedTankSettings.foodHealthRestorationBaseValue = tank.tankSettings.foodHealthRestorationBaseValue
managedTankSettings.foodSpawnRate = Int16(tank.tankSettings.foodSpawnRate)
managedTankSettings.creatureInitialAmount = Int16(tank.tankSettings.creatureInitialAmount)
managedTankSettings.creatureMinimumAmount = Int16(tank.tankSettings.creatureMinimumAmount)
managedTankSettings.creatureSpawnRate = Int16(tank.tankSettings.creatureSpawnRate)
managedTankSettings.creatureBirthSuccessRate = tank.tankSettings.creatureBirthSuccessRate
managedTankSettings.backgroundParticleBirthrate = Int16(tank.tankSettings.backgroundParticleBirthrate)
managedTankSettings.backgroundParticleLifetime = Int16(tank.tankSettings.backgroundParticleLifetime)

managedTank.tankSettings = managedTankSettings

try self.mainManagedObjectContext.save()

} catch {
Log.error("Tank failed to save to storage.")
}
}
}
}
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14903" systemVersion="18G95" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="ManagedBubble" representedClassName="ManagedBubble" syncable="YES">
<attribute name="positionX" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="positionY" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="tank" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedTank" inverseName="bubbles" inverseEntity="ManagedTank"/>
</entity>
<entity name="ManagedCreature" representedClassName="ManagedCreature" syncable="YES">
<attribute name="firstName" attributeType="String"/>
<attribute name="lastName" attributeType="String"/>
<attribute name="movementSpeed" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="primaryHue" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="sizeModifier" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="turnSpeed" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="uuid" attributeType="UUID" usesScalarValueType="NO"/>
<relationship name="limbs" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="ManagedLimb" inverseName="creature" inverseEntity="ManagedLimb"/>
</entity>
<entity name="ManagedFood" representedClassName="ManagedFood" syncable="YES">
<attribute name="positionX" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="positionY" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="tank" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedTank" inverseName="food" inverseEntity="ManagedTank"/>
</entity>
<entity name="ManagedLimb" representedClassName="ManagedLimb" syncable="YES">
<attribute name="blend" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="brightness" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="hue" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="limbWidth" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="limbzRotation" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="positionX" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="positionY" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="saturation" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="shape" attributeType="String"/>
<attribute name="wiggleActionBackDuration" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="wiggleActionDuration" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="wiggleActionMovementBackDuration" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="wiggleActionMovementDuration" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="wiggleFactor" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="wiggleMoveBackFactor" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="wiggleMoveFactor" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="creature" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedCreature" inverseName="limbs" inverseEntity="ManagedCreature"/>
</entity>
<entity name="ManagedTank" representedClassName="ManagedTank" syncable="YES">
<attribute name="birthCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="deathCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tankTime" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="timestamp" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uuid" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<relationship name="bubbles" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="ManagedBubble" inverseName="tank" inverseEntity="ManagedBubble"/>
<relationship name="creatures" toMany="YES" deletionRule="Cascade" destinationEntity="ManagedTankCreature" inverseName="tank" inverseEntity="ManagedTankCreature"/>
<relationship name="food" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="ManagedFood" inverseName="tank" inverseEntity="ManagedFood"/>
<relationship name="tankSettings" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedTankSettings" inverseName="tank" inverseEntity="ManagedTankSettings"/>
</entity>
<entity name="ManagedTankCreature" representedClassName="ManagedTankCreature" parentEntity="ManagedCreature" syncable="YES">
<attribute name="currentHealth" attributeType="Float" defaultValueString="0.0"/>
<attribute name="isFavorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lifeTime" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="positionX" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="positionY" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="tank" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedTank" inverseName="creatures" inverseEntity="ManagedTank"/>
</entity>
<entity name="ManagedTankSettings" representedClassName="ManagedTankSettings" syncable="YES">
<attribute name="backgroundParticleBirthrate" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="backgroundParticleLifetime" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="creatureBirthSuccessRate" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="creatureInitialAmount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="creatureMinimumAmount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="creatureSpawnRate" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="foodHealthRestorationBaseValue" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="foodMaxAmount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="foodSpawnRate" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="tank" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedTank" inverseName="tankSettings" inverseEntity="ManagedTank"/>
</entity>
<elements>
<element name="ManagedBubble" positionX="368.70703125" positionY="111.85546875" width="128" height="88"/>
<element name="ManagedCreature" positionX="-26.3359375" positionY="-118.1875" width="128" height="163"/>
<element name="ManagedFood" positionX="391.83203125" positionY="6.43359375" width="128" height="88"/>
<element name="ManagedTank" positionX="-200.9921875" positionY="48.17578125" width="128" height="178"/>
<element name="ManagedTankSettings" positionX="156.62890625" positionY="194.0703125" width="128" height="193"/>
<element name="ManagedTankCreature" positionX="149.62890625" positionY="-8.6640625" width="128" height="133"/>
<element name="ManagedLimb" positionX="9" positionY="36" width="128" height="298"/>
</elements>
</model>
@@ -0,0 +1,16 @@
//
// ManagedBubble+CoreDataClass.swift
// Aeon Garden
//
// Created by Brad Root on 10/6/19.
// Copyright © 2019 Brad Root. All rights reserved.
//
//

import Foundation
import CoreData

@objc(ManagedBubble)
public class ManagedBubble: NSManagedObject {

}
@@ -0,0 +1,30 @@
//
// ManagedBubble+CoreDataProperties.swift
// Aeon Garden
//
// Created by Brad Root on 10/6/19.
// Copyright © 2019 Brad Root. All rights reserved.
//
//

import Foundation
import CoreData


extension ManagedBubble {

@nonobjc public class func fetchRequest() -> NSFetchRequest<ManagedBubble> {
return NSFetchRequest<ManagedBubble>(entityName: "ManagedBubble")
}

@NSManaged public var positionX: Float
@NSManaged public var positionY: Float
@NSManaged public var tank: ManagedTank

}

extension ManagedBubble {
func toStruct() -> Bubble {
return Bubble(positionX: self.positionX, positionY: self.positionY)
}
}
@@ -0,0 +1,16 @@
//
// ManagedCreature+CoreDataClass.swift
// Aeon Garden
//
// Created by Brad Root on 10/6/19.
// Copyright © 2019 Brad Root. All rights reserved.
//
//

import Foundation
import CoreData

@objc(ManagedCreature)
public class ManagedCreature: NSManagedObject {

}

0 comments on commit 403a9e4

Please sign in to comment.