/* ecsjs is an entity component system library for JavaScript Copyright (C) 2014 Peter Flannery This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import type { ComponentMap } from './component-map.js'; import { ComponentClassesMapKey, type ComponentClass, type Iterator, } from './types.js'; import { hashString } from './utils.js'; /** * Component class map for storing registered component maps * @category Maps */ export class ComponentClassesMap { private hashIdToName: Map = new Map() private hashIdToIndex = new Map(); private indexToHashId: number[] = []; private maps: ComponentMap[] = []; /** * Creates a new component classes map * @param entries - Optional initial data for the map */ constructor(entries?: readonly (readonly [number, ComponentMap])[] | null) { if (entries) { for (let i = 0; i < entries.length; i++) { const entry = entries[i]; this.setByHashId(entry[0], entry[1]); } } } /** * Returns the number of registered component maps */ get size() { return this.maps.length; } /** * Clears all maps and internal indices while preserving array references */ clear(): void { this.maps.length = 0; this.indexToHashId.length = 0; this.hashIdToIndex.clear(); } /** * Retrieves a component map using the component class * @param componentClass - The component class to get the map for * @returns The {@link ComponentMap} for the specified class, or undefined if not found */ get(componentClass: ComponentClass): ComponentMap | undefined { const index = this.hashIdToIndex.get(componentClass.hashId!); return index !== undefined ? this.maps[index] : undefined; } /** * Checks if a component class map is already registered * @param componentClass - The component class to check * @returns True if the map is registered, otherwise false */ has(componentClass: ComponentClass): boolean { if (componentClass.hashId === undefined) return false; return this.hashIdToIndex.has(componentClass.hashId); } /** * Registers a component map. Automatically generates a hashId from the class name if missing * @param componentClass - The component class to register * @param map - The {@link ComponentMap} instance to store * @returns The {@link ComponentClassesMap} instance for chaining */ set(componentClass: ComponentClass, map: ComponentMap): this { if (componentClass.hashId === undefined) { componentClass.hashId = hashString(componentClass.name); } this.hashIdToName.set(componentClass.hashId, componentClass.name); return this.setByHashId(componentClass.hashId!, map); } /** * Maps a specific numeric hash id to a component map instance. * @param hash - The numeric hash id * @param map - The {@link ComponentMap} instance to store * @returns The {@link ComponentClassesMap} instance for chaining */ setByHashId(hash: number, map: ComponentMap): this { const existingIndex = this.hashIdToIndex.get(hash); if (existingIndex !== undefined) { this.maps[existingIndex] = map; } else { this.hashIdToIndex.set(hash, this.maps.length); this.indexToHashId.push(hash); this.maps.push(map); } return this; } /** * Removes a component map and re-orders internal storage to maintain density * @param componentClass - The component class to remove the map for * @returns True if the map was removed, otherwise false */ delete(componentClass: ComponentClass): boolean { const hashId = componentClass.hashId!; const index = this.hashIdToIndex.get(hashId); if (index === undefined) return false; const lastIndex = this.maps.length - 1; const lastHash = this.indexToHashId[lastIndex]; // Swap this.maps[index] = this.maps[lastIndex]; this.indexToHashId[index] = lastHash; // Update map pointer this.hashIdToIndex.set(lastHash, index); // Pop this.maps.pop(); this.indexToHashId.pop(); this.hashIdToIndex.delete(hashId); return true; } /** * Executes a callback for every map, providing the instance and its registered name * @param callback - The callback to execute */ forEach(callback: (value: ComponentMap, name: string) => void) { for (let i = 0; i < this.maps.length; i++) { callback(this.maps[i], this.hashIdToName.get(this.indexToHashId[i])!); } } /** * Returns an iterator of all component map instances * @returns An {@link ArrayIterator} of {@link ComponentMap} instances */ values(): ArrayIterator> { return this.maps.values() } /** * Returns an iterator of [hashId, componentMap] pairs * @returns An {@link Iterator} instance */ entries(): Iterator<[number, ComponentMap]> { let index = 0; const instances = this.maps; const hashes = this.indexToHashId; return { next(): IteratorResult<[number, ComponentMap]> { if (index < instances.length) { const value: [number, ComponentMap] = [hashes[index], instances[index]]; index++; return { value, done: false }; } return { value: undefined as any, done: true }; }, reset() { index = 0; }, [Symbol.iterator]() { return this; } }; } /** * Called when using JSON.stringify * @returns An object representation for serialization */ toJSON() { return { [ComponentClassesMapKey]: 1, iterable: [...this.entries()] }; } }