diff --git a/Sources/FirebladeECS/CodingStrategy.swift b/Sources/FirebladeECS/CodingStrategy.swift index 17906370..4e464761 100644 --- a/Sources/FirebladeECS/CodingStrategy.swift +++ b/Sources/FirebladeECS/CodingStrategy.swift @@ -5,7 +5,7 @@ // Created by Christian Treffs on 05.08.20. // -public protocol CodingStrategy { +public protocol CodingStrategy: Codable { func codingKey(for componentType: C.Type) -> DynamicCodingKey where C: Component } diff --git a/Sources/FirebladeECS/ComponentIdentifier.swift b/Sources/FirebladeECS/ComponentIdentifier.swift index 79e62476..09ee15f7 100644 --- a/Sources/FirebladeECS/ComponentIdentifier.swift +++ b/Sources/FirebladeECS/ComponentIdentifier.swift @@ -21,6 +21,17 @@ extension ComponentIdentifier { static func makeRuntimeHash(_ componentType: (some Component).Type) -> Identifier { ObjectIdentifier(componentType).hashValue } + + typealias StableId = UInt64 + static func makeStableTypeHash(component: Component) -> StableId { + let componentTypeString = String(describing: type(of: component)) + return StringHashing.singer_djb2(componentTypeString) + } + + static func makeStableInstanceHash(component: Component, entityId: EntityIdentifier) -> StableId { + let componentTypeString = String(describing: type(of: component)) + String(entityId.id) + return StringHashing.singer_djb2(componentTypeString) + } } extension ComponentIdentifier: Equatable {} diff --git a/Sources/FirebladeECS/EntityIdentifierGenerator.swift b/Sources/FirebladeECS/EntityIdentifierGenerator.swift index 1d6434bc..d92a7256 100644 --- a/Sources/FirebladeECS/EntityIdentifierGenerator.swift +++ b/Sources/FirebladeECS/EntityIdentifierGenerator.swift @@ -11,7 +11,7 @@ /// It also allows entity ids to be marked as unused (to be re-usable). /// /// You should strive to keep entity ids tightly packed around `EntityIdentifier.Identifier.min` since it has an influence on the underlying memory layout. -public protocol EntityIdentifierGenerator { +public protocol EntityIdentifierGenerator: Codable { /// Initialize the generator providing entity ids to begin with when creating new entities. /// /// Entity ids provided should be passed to `nextId()` in last out order up until the collection is empty. @@ -101,3 +101,5 @@ public struct LinearIncrementingEntityIdGenerator: EntityIdentifierGenerator { storage.markUnused(entityId: entityId) } } + +extension LinearIncrementingEntityIdGenerator.Storage: Codable {} diff --git a/Sources/FirebladeECS/Nexus+Codable.swift b/Sources/FirebladeECS/Nexus+Codable.swift new file mode 100644 index 00000000..f00c83b9 --- /dev/null +++ b/Sources/FirebladeECS/Nexus+Codable.swift @@ -0,0 +1,24 @@ +// +// Nexus+Codable.swift +// +// +// Created by Christian Treffs on 14.07.20. +// + +extension Nexus: Encodable { + public func encode(to encoder: Encoder) throws { + let serializedNexus = try serialize() + var container = encoder.singleValueContainer() + try container.encode(serializedNexus) + } +} + +extension Nexus: Decodable { + public convenience init(from decoder: Decoder) throws { + self.init() + + let container = try decoder.singleValueContainer() + let sNexus = try container.decode(SNexus.self) + try deserialize(from: sNexus, into: self) + } +} diff --git a/Sources/FirebladeECS/Nexus+Serialization.swift b/Sources/FirebladeECS/Nexus+Serialization.swift new file mode 100644 index 00000000..c9073b4a --- /dev/null +++ b/Sources/FirebladeECS/Nexus+Serialization.swift @@ -0,0 +1,127 @@ +// +// Nexus+Serialization.swift +// +// +// Created by Christian Treffs on 26.06.20. +// + +#if canImport(Foundation) + import struct Foundation.Data + + extension Nexus { + final func serialize() throws -> SNexus { + var componentInstances: [ComponentIdentifier.StableId: SComponent] = [:] + var entityComponentsMap: [EntityIdentifier: Set] = [:] + + for entitId in componentIdsByEntity.keys { + entityComponentsMap[entitId] = [] + let componentIds = self.get(components: entitId) ?? [] + + for componentId in componentIds { + let component = self.get(unsafe: componentId, for: entitId) + let componentStableInstanceHash = ComponentIdentifier.makeStableInstanceHash(component: component, entityId: entitId) + componentInstances[componentStableInstanceHash] = SComponent.component(component) + entityComponentsMap[entitId]?.insert(componentStableInstanceHash) + } + } + + return SNexus(version: version, + entities: entityComponentsMap, + components: componentInstances) + } + + final func deserialize(from sNexus: SNexus, into nexus: Nexus) throws { + for freeId in sNexus.entities.map(\.key).reversed() { + nexus.entityIdGenerator.markUnused(entityId: freeId) + } + + for componentSet in sNexus.entities.values { + let entity = createEntity() + for sCompId in componentSet { + guard let sComp = sNexus.components[sCompId] else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Could not find component instance for \(sCompId).")) + } + + switch sComp { + case let .component(comp): + entity.assign(comp) + } + } + } + } + } + + extension EntityIdentifier: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(id) + } + } + + extension EntityIdentifier: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let id = try container.decode(UInt32.self) + self.init(id) + } + } + + struct SNexus { + let version: Version + let entities: [EntityIdentifier: Set] + let components: [ComponentIdentifier.StableId: SComponent] + } + + extension SNexus: Encodable {} + extension SNexus: Decodable {} + + protocol ComponentEncoding { + static func encode(component: Component, to encoder: Encoder) throws + } + + protocol ComponentDecoding { + static func decode(from decoder: Decoder) throws -> Component + } + + typealias ComponentCodable = ComponentDecoding & ComponentEncoding + + extension SNexus: ComponentEncoding { + static func encode(component: Component, to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + let bytes = withUnsafeBytes(of: component) { + Data(bytes: $0.baseAddress!, count: MemoryLayout.stride(ofValue: component)) + } + try container.encode(bytes) + } + } + + extension SNexus: ComponentDecoding { + static func decode(from decoder: Decoder) throws -> Component { + let container = try decoder.singleValueContainer() + let instanceData = try container.decode(Data.self) + return instanceData.withUnsafeBytes { + $0.baseAddress!.load(as: Component.self) + } + } + } + + enum SComponent { + case component(Component) + } + + extension SComponent: Encodable { + public func encode(to encoder: Encoder) throws { + switch self { + case let .component(comp): + try CodingStrategy.encode(component: comp, to: encoder) + } + } + } + + extension SComponent: Decodable { + public init(from decoder: Decoder) throws { + self = try .component(CodingStrategy.decode(from: decoder)) + } + } + +#endif diff --git a/Sources/FirebladeECS/Nexus.swift b/Sources/FirebladeECS/Nexus.swift index 554d84af..7f0f413a 100644 --- a/Sources/FirebladeECS/Nexus.swift +++ b/Sources/FirebladeECS/Nexus.swift @@ -6,6 +6,11 @@ // public final class Nexus { + /// The version of this Nexus implementation. + /// + /// Used for serialization. + final let version = Version(0, 18, 0) + /// - Key: ComponentIdentifier aka component type. /// - Value: Array of component instances of same type (uniform). /// New component instances are appended. diff --git a/Sources/FirebladeECS/Version.swift b/Sources/FirebladeECS/Version.swift new file mode 100644 index 00000000..f7f8a83e --- /dev/null +++ b/Sources/FirebladeECS/Version.swift @@ -0,0 +1,164 @@ +// +// Version.swift +// +// +// Created by Christian Treffs on 14.07.20. +// + +/// A struct representing a semantic version. +/// +/// See for details. +public struct Version { + /// Major version. + public let major: UInt + + /// Minor version. + public let minor: UInt + + /// Patch version. + public let patch: UInt + + /// Pre-release identifiers. + public let prereleaseIdentifiers: [String] + + /// Build metadata identifiers. + public let buildMetadataIdentifiers: [String] + + public init(_ major: UInt, _ minor: UInt, _ patch: UInt, prereleaseIdentifiers: [String] = [], buildMetadataIdentifiers: [String] = []) { + self.major = major + self.minor = minor + self.patch = patch + self.prereleaseIdentifiers = prereleaseIdentifiers + self.buildMetadataIdentifiers = buildMetadataIdentifiers + } + + public init?(decoding versionString: String) { + let prereleaseStartIndex = versionString.firstIndex(of: "-") + let metadataStartIndex = versionString.firstIndex(of: "+") + + let requiredEndIndex = prereleaseStartIndex ?? metadataStartIndex ?? versionString.endIndex + let requiredCharacters = versionString.prefix(upTo: requiredEndIndex) + let requiredComponents: [UInt] = requiredCharacters + .split(separator: ".", maxSplits: 2, omittingEmptySubsequences: false) + .map(String.init) + .compactMap(UInt.init) + + guard requiredComponents.count == 3 else { return nil } + + major = requiredComponents[0] + minor = requiredComponents[1] + patch = requiredComponents[2] + + func identifiers(start: String.Index?, end: String.Index) -> [String] { + guard let start else { return [] } + let identifiers = versionString[versionString.index(after: start) ..< end] + return identifiers.split(separator: ".").map(String.init) + } + + prereleaseIdentifiers = identifiers( + start: prereleaseStartIndex, + end: metadataStartIndex ?? versionString.endIndex + ) + buildMetadataIdentifiers = identifiers( + start: metadataStartIndex, + end: versionString.endIndex + ) + } + + public var versionString: String { + var versionString = "\(major).\(minor).\(patch)" + if !prereleaseIdentifiers.isEmpty { + versionString += "-" + prereleaseIdentifiers.joined(separator: ".") + } + if !buildMetadataIdentifiers.isEmpty { + versionString += "+" + buildMetadataIdentifiers.joined(separator: ".") + } + return versionString + } +} + +extension Version: Equatable {} +extension Version: Comparable { + func isEqualWithoutPrerelease(_ other: Version) -> Bool { + major == other.major && minor == other.minor && patch == other.patch + } + + public static func < (lhs: Version, rhs: Version) -> Bool { + let lhsComparators = [lhs.major, lhs.minor, lhs.patch] + let rhsComparators = [rhs.major, rhs.minor, rhs.patch] + + if lhsComparators != rhsComparators { + return lhsComparators.lexicographicallyPrecedes(rhsComparators) + } + + guard !lhs.prereleaseIdentifiers.isEmpty else { + return false // Non-prerelease lhs >= potentially prerelease rhs + } + + guard !rhs.prereleaseIdentifiers.isEmpty else { + return true // Prerelease lhs < non-prerelease rhs + } + + let zippedIdentifiers = zip(lhs.prereleaseIdentifiers, rhs.prereleaseIdentifiers) + for (lhsPrereleaseIdentifier, rhsPrereleaseIdentifier) in zippedIdentifiers { + if lhsPrereleaseIdentifier == rhsPrereleaseIdentifier { + continue + } + + let typedLhsIdentifier: Any = Int(lhsPrereleaseIdentifier) ?? lhsPrereleaseIdentifier + let typedRhsIdentifier: Any = Int(rhsPrereleaseIdentifier) ?? rhsPrereleaseIdentifier + + switch (typedLhsIdentifier, typedRhsIdentifier) { + case let (int1 as Int, int2 as Int): return int1 < int2 + case let (string1 as String, string2 as String): return string1 < string2 + case (is Int, is String): return true // Int prereleases < String prereleases + case (is String, is Int): return false + default: + return false + } + } + + return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count + } +} + +extension Version: ExpressibleByStringLiteral { + public init(stringLiteral versionString: String) { + guard let version = Version(decoding: versionString) else { + fatalError("Malformed version string '\(versionString)'.") + } + self = version + } +} + +extension Version: CustomStringConvertible { + public var description: String { + versionString + } +} + +extension Version: CustomDebugStringConvertible { + public var debugDescription: String { + versionString + } +} + +extension Version: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let versionString = try container.decode(String.self) + guard let version = Version(decoding: versionString) else { + throw DecodingError.dataCorruptedError(in: container, + debugDescription: "Malformed version string '\(versionString)'.") + } + + self = version + } +} + +extension Version: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(versionString) + } +} diff --git a/Tests/FirebladeECSTests/SerializationTests.swift b/Tests/FirebladeECSTests/SerializationTests.swift new file mode 100644 index 00000000..b32d3d56 --- /dev/null +++ b/Tests/FirebladeECSTests/SerializationTests.swift @@ -0,0 +1,75 @@ +// +// SerializationTests.swift +// +// +// Created by Christian Treffs on 26.06.20. +// + +import XCTest +import FirebladeECS + +public final class SerializationTests: XCTestCase { + func testSerialization() throws { + let nexus = Nexus() + let e1 = nexus.createEntity() + let e2 = nexus.createEntity() + nexus.createEntity(with: Position(x: 1, y: 4), Name(name: "myName")) + let e3 = nexus.createEntity() + nexus.createEntity(with: Position(x: 5, y: 18), Name(name: "yourName")) + + // Fragment entities + nexus.destroy(entity: e2) + nexus.destroy(entity: e3) + nexus.destroy(entity: e1) + + let encoder = JSONEncoder() + let data = try encoder.encode(nexus) + + XCTAssertNotNil(data) + XCTAssertGreaterThanOrEqual(data.count, 307) + + let encoder2 = JSONEncoder() + let data2 = try encoder2.encode(nexus) + XCTAssertEqual(data.count, data2.count) + //print(String(data: data, encoding: .utf8)!) + //print(String(data: data2, encoding: .utf8)!) + } + + func testDeserialization() throws { + let nexus = Nexus() + let e1 = nexus.createEntity() + let e2 = nexus.createEntity() + let firstEntity = nexus.createEntity(with: Name(name: "myName"), Position(x: 1, y: 2)) + let e3 = nexus.createEntity() + let secondEntity = nexus.createEntity(with: Velocity(a: 3.14), Party(partying: true)) + + // Fragment entities + nexus.destroy(entity: e2) + nexus.destroy(entity: e3) + nexus.destroy(entity: e1) + + let encoder = JSONEncoder() + let data = try encoder.encode(nexus) + + let decoder = JSONDecoder() + let nexus2: Nexus = try decoder.decode(Nexus.self, from: data) + + let firstEntity2 = nexus2.entity(from: firstEntity.identifier) + XCTAssertEqual(firstEntity2.identifier, firstEntity.identifier) + XCTAssertTrue(firstEntity2.has(Name.self)) + XCTAssertTrue(firstEntity2.has(Position.self)) + XCTAssertEqual(firstEntity2.get(component: Name.self)?.name, "myName") + XCTAssertEqual(firstEntity2.get(component: Position.self)?.x, 1) + XCTAssertEqual(firstEntity2.get(component: Position.self)?.y, 2) + + let secondEntity2 = nexus2.entity(from: secondEntity.identifier) + XCTAssertEqual(secondEntity2.identifier, secondEntity.identifier) + XCTAssertTrue(secondEntity2.has(Velocity.self)) + XCTAssertTrue(secondEntity2.has(Party.self)) + XCTAssertEqual(secondEntity2.get(component: Velocity.self)?.a, 3.14) + XCTAssertEqual(secondEntity2.get(component: Party.self)?.partying, true) + + XCTAssertEqual(nexus2.numEntities, nexus.numEntities) + XCTAssertEqual(nexus2.numComponents, nexus.numComponents) + } +}