Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nexus serialization #38

Draft
wants to merge 40 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
67b2dbf
WIP: encoding
ctreffs Jun 26, 2020
f033421
Encoding works
ctreffs Jun 26, 2020
3d994f0
WIP: deserialization
ctreffs Jun 26, 2020
8c6211e
Decoding working - unstable identifiers
ctreffs Jun 26, 2020
b37c4ce
Remove unused components types
ctreffs Jun 26, 2020
7176780
Stabilize encoding
ctreffs Jun 26, 2020
d76e988
Rework entityId generation
ctreffs Jun 26, 2020
4f9e6bf
Add serialization to library
ctreffs Jun 26, 2020
49d0c8f
Refine format
ctreffs Jun 26, 2020
3980d8f
Merge branch 'develop' into feature/serialization
ctreffs Jun 26, 2020
30ab23e
Merge branch 'develop' into feature/serialization
ctreffs Jun 28, 2020
350ced4
Merge branch 'develop' into feature/serialization
ctreffs Jul 9, 2020
2fbbf42
Cleanup
ctreffs Jul 9, 2020
058ca5f
Merge branch 'develop' into feature/serialization
ctreffs Jul 10, 2020
9fc12c4
Cleanup serialization
ctreffs Jul 14, 2020
6f05917
Merge branch 'develop' into feature/serialization
ctreffs Jul 16, 2020
ee870d1
Merge branch 'develop' into feature/serialization
ctreffs Jul 21, 2020
c691559
Merge branch 'develop' into feature/serialization
ctreffs Aug 6, 2020
0129974
Merge branch 'develop' into feature/serialization
ctreffs Aug 11, 2020
ea76df0
Merge branch 'develop' into feature/serialization
ctreffs Aug 20, 2020
762e845
Merge branch 'develop' into feature/serialization
ctreffs Aug 21, 2020
5ebf44a
Ensure stable entities over serialization
ctreffs Aug 21, 2020
6b00d67
Merge branch 'feature/entity-id-gen' into feature/serialization
ctreffs Aug 21, 2020
8b27d40
Merge branch 'feature/entity-id-gen' into feature/serialization
ctreffs Aug 21, 2020
2bce298
Use new entity id generator
ctreffs Aug 21, 2020
dc1d2f6
Merge branch 'develop' into feature/serialization
ctreffs Aug 21, 2020
2e6ecdf
Lint
ctreffs Aug 21, 2020
5a33f1d
Merge branch 'develop' into feature/serialization
ctreffs Aug 21, 2020
64aea76
Update to new storage
ctreffs Aug 21, 2020
6b128a9
Merge branch 'develop' into feature/serialization
ctreffs Aug 22, 2020
fc688a6
Merge branch 'develop' into feature/serialization
ctreffs Aug 28, 2020
995ff5a
Merge branch 'develop' into feature/serialization
ctreffs Oct 8, 2020
c88fe2c
Merge branch 'develop' into feature/serialization
ctreffs Oct 21, 2020
2333282
Fix compile issue
ctreffs Oct 21, 2020
40d09a9
Conform CodingStrategy and EntityIdentifierGenerator to Codable
ctreffs Oct 21, 2020
168e1a8
Refine Version
ctreffs Oct 21, 2020
dadfeb8
Refine Nexus serialization
ctreffs Oct 21, 2020
c4c3cce
Lint
ctreffs Oct 21, 2020
481b991
Merge branch 'develop' into feature/serialization
ctreffs Nov 25, 2020
df99994
Merge branch 'master' into feature/serialization
ctreffs Nov 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/FirebladeECS/CodingStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Christian Treffs on 05.08.20.
//

public protocol CodingStrategy {
public protocol CodingStrategy: Codable {
func codingKey<C>(for componentType: C.Type) -> DynamicCodingKey where C: Component
}

Expand Down
11 changes: 11 additions & 0 deletions Sources/FirebladeECS/ComponentIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ extension ComponentIdentifier {
internal static func makeRuntimeHash<C>(_ componentType: C.Type) -> Identifier where C: Component {
ObjectIdentifier(componentType).hashValue
}

typealias StableId = UInt64
internal static func makeStableTypeHash(component: Component) -> StableId {
let componentTypeString = String(describing: type(of: component))
return StringHashing.singer_djb2(componentTypeString)
}

internal 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 { }
Expand Down
3 changes: 2 additions & 1 deletion Sources/FirebladeECS/EntityIdentifierGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -101,3 +101,4 @@ public struct LinearIncrementingEntityIdGenerator: EntityIdentifierGenerator {
storage.markUnused(entityId: entityId)
}
}
extension LinearIncrementingEntityIdGenerator.Storage: Codable { }
24 changes: 24 additions & 0 deletions Sources/FirebladeECS/Nexus+Codable.swift
Original file line number Diff line number Diff line change
@@ -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 self.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)
}
}
124 changes: 124 additions & 0 deletions Sources/FirebladeECS/Nexus+Serialization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//
// 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<SNexus>] = [:]
var entityComponentsMap: [EntityIdentifier: Set<ComponentIdentifier.StableId>] = [:]

for entitId in self.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 { $0.key }.reversed() {
nexus.entityIdGenerator.markUnused(entityId: freeId)
}

for componentSet in sNexus.entities.values {
let entity = self.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)
}
}

internal struct SNexus {
let version: Version
let entities: [EntityIdentifier: Set<ComponentIdentifier.StableId>]
let components: [ComponentIdentifier.StableId: SComponent<SNexus>]
}
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 = ComponentEncoding & ComponentDecoding

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<CodingStrategy: ComponentCodable> {
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 = .component(try CodingStrategy.decode(from: decoder))
}
}

#endif
5 changes: 5 additions & 0 deletions Sources/FirebladeECS/Nexus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
163 changes: 163 additions & 0 deletions Sources/FirebladeECS/Version.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//
// Version.swift
//
//
// Created by Christian Treffs on 14.07.20.
//

/// A struct representing a semantic version.
///
/// See <https://semver.org> 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 }

self.major = requiredComponents[0]
self.minor = requiredComponents[1]
self.patch = requiredComponents[2]

func identifiers(start: String.Index?, end: String.Index) -> [String] {
guard let start = start else { return [] }
let identifiers = versionString[versionString.index(after: start)..<end]
return identifiers.split(separator: ".").map(String.init)
}

self.prereleaseIdentifiers = identifiers(
start: prereleaseStartIndex,
end: metadataStartIndex ?? versionString.endIndex)
self.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.count > 0 else {
return false // Non-prerelease lhs >= potentially prerelease rhs
}

guard rhs.prereleaseIdentifiers.count > 0 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)
}
}
Loading