class_name WorldEventDailyRollupStore extends RefCounted const SCHEMA_VERSION := 1 const RECORD_FIELDS := ["schema_version", "rollups"] var _records_by_key: Dictionary = {} var _ordered_keys: Array[String] = [] var _day_index: Dictionary = {} var _location_index: Dictionary = {} var _event_type_index: Dictionary = {} var _item_index: Dictionary = {} static func from_dictionary(store_data: Dictionary) -> WorldEventDailyRollupStore: var store := WorldEventDailyRollupStore.new() return store if store.restore_from_dictionary(store_data) else null func size() -> int: return _records_by_key.size() func is_empty() -> bool: return _records_by_key.is_empty() func has_rollup( day_index: int, world_id: StringName, location_id: StringName, event_type: StringName, item_id: StringName ) -> bool: return _records_by_key.has( WorldEventDailyRollupRecord.index_key_for( day_index, world_id, location_id, event_type, item_id ) ) func append(record: WorldEventDailyRollupRecord) -> bool: return _append(record) func get_by_key( day_index: int, world_id: StringName, location_id: StringName, event_type: StringName, item_id: StringName ) -> WorldEventDailyRollupRecord: var key := WorldEventDailyRollupRecord.index_key_for( day_index, world_id, location_id, event_type, item_id ) return _records_by_key.get(key) as WorldEventDailyRollupRecord func get_all() -> Array[WorldEventDailyRollupRecord]: return _records_for_keys(_ordered_keys) func get_for_day(day_index: int) -> Array[WorldEventDailyRollupRecord]: return _records_for_keys(_day_index.get(day_index, [])) func get_for_location( world_id: StringName, location_id: StringName ) -> Array[WorldEventDailyRollupRecord]: return _records_for_keys( _location_index.get(WorldEventDailyRollupRecord.location_key_for(world_id, location_id), []) ) func get_for_event_type(event_type: StringName) -> Array[WorldEventDailyRollupRecord]: return _records_for_keys(_event_type_index.get(event_type, [])) func get_for_item(item_id: StringName) -> Array[WorldEventDailyRollupRecord]: return _records_for_keys(_item_index.get(item_id, [])) func restore(records: Array[WorldEventDailyRollupRecord]) -> bool: var candidate := WorldEventDailyRollupStore.new() for record: WorldEventDailyRollupRecord in records: if not candidate._append(record): return false _adopt(candidate) return true func restore_from_dictionary(store_data: Dictionary) -> bool: if not _has_exact_fields(store_data, RECORD_FIELDS): return false if ( not store_data["schema_version"] is int or int(store_data["schema_version"]) != SCHEMA_VERSION or not store_data["rollups"] is Array ): return false var records: Array[WorldEventDailyRollupRecord] = [] for raw_record: Variant in store_data["rollups"]: if not raw_record is Dictionary: return false var record := WorldEventDailyRollupRecord.from_dictionary(raw_record) if record == null: return false records.append(record) if not _records_are_canonical(records): return false return restore(records) func to_dictionary() -> Dictionary: var serialized: Array[Dictionary] = [] for record: WorldEventDailyRollupRecord in get_all(): serialized.append(record.to_dictionary()) return { "schema_version": SCHEMA_VERSION, "rollups": serialized, } func checksum() -> String: return JSON.stringify(to_dictionary()).sha256_text() func _append(record: WorldEventDailyRollupRecord) -> bool: if record == null or not record.is_valid() or _records_by_key.has(record.index_key()): return false var stored := WorldEventDailyRollupRecord.from_dictionary(record.to_dictionary()) if stored == null: return false var key := stored.index_key() _records_by_key[key] = stored _insert_key(_ordered_keys, key) _index_key(_day_index, stored.get_day_index(), key) _index_key(_location_index, stored.location_index_key(), key) _index_key(_event_type_index, stored.get_event_type(), key) _index_key(_item_index, stored.get_item_id(), key) return true func _index_key(index: Dictionary, index_value: Variant, record_key: String) -> void: if not index.has(index_value): index[index_value] = [] var keys: Array = index[index_value] _insert_key(keys, record_key) func _insert_key(keys: Array, record_key: String) -> void: var low := 0 var high := keys.size() while low < high: var middle := (low + high) / 2 if _record_precedes(record_key, String(keys[middle])): high = middle else: low = middle + 1 keys.insert(low, record_key) func _record_precedes(first_key: String, second_key: String) -> bool: var first := _records_by_key[first_key] as WorldEventDailyRollupRecord var second := _records_by_key[second_key] as WorldEventDailyRollupRecord if first.get_day_index() != second.get_day_index(): return first.get_day_index() < second.get_day_index() if first.get_world_id() != second.get_world_id(): return String(first.get_world_id()) < String(second.get_world_id()) if first.get_location_id() != second.get_location_id(): return String(first.get_location_id()) < String(second.get_location_id()) if first.get_event_type() != second.get_event_type(): return String(first.get_event_type()) < String(second.get_event_type()) return String(first.get_item_id()) < String(second.get_item_id()) func _records_for_keys(keys: Array) -> Array[WorldEventDailyRollupRecord]: var records: Array[WorldEventDailyRollupRecord] = [] for key: Variant in keys: var record := _records_by_key.get(String(key)) as WorldEventDailyRollupRecord if record != null: records.append(record) return records func _records_are_canonical(records: Array[WorldEventDailyRollupRecord]) -> bool: var candidate := WorldEventDailyRollupStore.new() for record: WorldEventDailyRollupRecord in records: if not candidate._append(record): return false var canonical_records := candidate.get_all() if canonical_records.size() != records.size(): return false for index in range(records.size()): if canonical_records[index].index_key() != records[index].index_key(): return false return true func _adopt(source: WorldEventDailyRollupStore) -> void: _records_by_key = source._records_by_key _ordered_keys = source._ordered_keys _day_index = source._day_index _location_index = source._location_index _event_type_index = source._event_type_index _item_index = source._item_index static func _has_exact_fields(record_data: Dictionary, fields: Array) -> bool: if record_data.size() != fields.size(): return false for field: String in fields: if not record_data.has(field): return false return true